Finalize cross-service upload workflow.
This commit is contained in:
parent
ab3cf591c6
commit
ffe1d9bd40
|
@ -8,5 +8,5 @@ public record SubmissionPayload(
|
||||||
float weight,
|
float weight,
|
||||||
String weightUnit,
|
String weightUnit,
|
||||||
int reps,
|
int reps,
|
||||||
String videoFileId
|
long taskId
|
||||||
) {}
|
) {}
|
||||||
|
|
|
@ -32,13 +32,24 @@ public class Submission {
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private LocalDateTime performedAt;
|
private LocalDateTime performedAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the video processing task that a user gives to us when they
|
||||||
|
* create the submission, so that when the task finishes processing, we can
|
||||||
|
* route its data to the right submission.
|
||||||
|
*/
|
||||||
|
@Column(nullable = false, updatable = false)
|
||||||
|
private long videoProcessingTaskId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id of the video file that was submitted for this submission. It lives
|
* The id of the video file that was submitted for this submission. It lives
|
||||||
* on the <em>gymboard-cdn</em> service as a stored file, which can be
|
* on the <em>gymboard-cdn</em> service as a stored file, which can be
|
||||||
* accessed via <code>GET https://CDN-HOST/files/{videoFileId}</code>.
|
* accessed via <code>GET https://CDN-HOST/files/{videoFileId}</code>.
|
||||||
*/
|
*/
|
||||||
@Column(nullable = false, updatable = false, length = 26)
|
@Column(length = 26)
|
||||||
private String videoFileId;
|
private String videoFileId = null;
|
||||||
|
|
||||||
|
@Column(length = 26)
|
||||||
|
private String thumbnailFileId = null;
|
||||||
|
|
||||||
@Column(nullable = false, precision = 7, scale = 2)
|
@Column(nullable = false, precision = 7, scale = 2)
|
||||||
private BigDecimal rawWeight;
|
private BigDecimal rawWeight;
|
||||||
|
@ -64,7 +75,7 @@ public class Submission {
|
||||||
Exercise exercise,
|
Exercise exercise,
|
||||||
User user,
|
User user,
|
||||||
LocalDateTime performedAt,
|
LocalDateTime performedAt,
|
||||||
String videoFileId,
|
long videoProcessingTaskId,
|
||||||
BigDecimal rawWeight,
|
BigDecimal rawWeight,
|
||||||
WeightUnit unit,
|
WeightUnit unit,
|
||||||
BigDecimal metricWeight,
|
BigDecimal metricWeight,
|
||||||
|
@ -73,9 +84,9 @@ public class Submission {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.gym = gym;
|
this.gym = gym;
|
||||||
this.exercise = exercise;
|
this.exercise = exercise;
|
||||||
this.videoFileId = videoFileId;
|
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.performedAt = performedAt;
|
this.performedAt = performedAt;
|
||||||
|
this.videoProcessingTaskId = videoProcessingTaskId;
|
||||||
this.rawWeight = rawWeight;
|
this.rawWeight = rawWeight;
|
||||||
this.weightUnit = unit;
|
this.weightUnit = unit;
|
||||||
this.metricWeight = metricWeight;
|
this.metricWeight = metricWeight;
|
||||||
|
@ -99,10 +110,26 @@ public class Submission {
|
||||||
return exercise;
|
return exercise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getVideoProcessingTaskId() {
|
||||||
|
return videoProcessingTaskId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getVideoFileId() {
|
public String getVideoFileId() {
|
||||||
return videoFileId;
|
return videoFileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getThumbnailFileId() {
|
||||||
|
return thumbnailFileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVideoFileId(String videoFileId) {
|
||||||
|
this.videoFileId = videoFileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setThumbnailFileId(String thumbnailFileId) {
|
||||||
|
this.thumbnailFileId = thumbnailFileId;
|
||||||
|
}
|
||||||
|
|
||||||
public User getUser() {
|
public User getUser() {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,4 +50,14 @@ public class CdnClient {
|
||||||
HttpResponse<String> response = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
return objectMapper.readValue(response.body(), responseType);
|
return objectMapper.readValue(response.body(), responseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void post(String urlPath) throws IOException, InterruptedException {
|
||||||
|
HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + urlPath))
|
||||||
|
.POST(HttpRequest.BodyPublishers.noBody())
|
||||||
|
.build();
|
||||||
|
HttpResponse<Void> response = httpClient.send(req, HttpResponse.BodyHandlers.discarding());
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
throw new IOException("Request failed with code " + response.statusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,12 @@ package nl.andrewlalis.gymboard_api.domains.api.service.cdn_client;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public record UploadsClient(CdnClient client) {
|
public record UploadsClient(CdnClient client) {
|
||||||
public record FileUploadResponse(String id) {}
|
public record FileUploadResponse(long taskId) {}
|
||||||
public record VideoProcessingTaskStatusResponse(String status) {}
|
public record VideoProcessingTaskStatusResponse(
|
||||||
|
String status,
|
||||||
|
String videoFileId,
|
||||||
|
String thumbnailFileId
|
||||||
|
) {}
|
||||||
|
|
||||||
public record FileMetadataResponse(
|
public record FileMetadataResponse(
|
||||||
String filename,
|
String filename,
|
||||||
|
@ -14,15 +18,19 @@ public record UploadsClient(CdnClient client) {
|
||||||
boolean availableForDownload
|
boolean availableForDownload
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public FileUploadResponse uploadVideo(Path filePath, String contentType) throws Exception {
|
public long uploadVideo(Path filePath, String contentType) throws Exception {
|
||||||
return client.postFile("/uploads/video", filePath, contentType, FileUploadResponse.class);
|
return client.postFile("/uploads/video", filePath, contentType, FileUploadResponse.class).taskId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public VideoProcessingTaskStatusResponse getVideoProcessingStatus(String id) throws Exception {
|
public VideoProcessingTaskStatusResponse getVideoProcessingTaskStatus(long id) throws Exception {
|
||||||
return client.get("/uploads/video/" + id + "/status", VideoProcessingTaskStatusResponse.class);
|
return client.get("/uploads/video/" + id + "/status", VideoProcessingTaskStatusResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileMetadataResponse getFileMetadata(String id) throws Exception {
|
public FileMetadataResponse getFileMetadata(String id) throws Exception {
|
||||||
return client.get("/files/" + id + "/metadata", FileMetadataResponse.class);
|
return client.get("/files/" + id + "/metadata", FileMetadataResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void startTask(long taskId) throws Exception {
|
||||||
|
client.post("/uploads/video/" + taskId + "/start");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,9 +91,14 @@ public class ExerciseSubmissionService {
|
||||||
Submission submission = submissionRepository.saveAndFlush(new Submission(
|
Submission submission = submissionRepository.saveAndFlush(new Submission(
|
||||||
ulid.nextULID(), gym, exercise, user,
|
ulid.nextULID(), gym, exercise, user,
|
||||||
performedAt,
|
performedAt,
|
||||||
payload.videoFileId(),
|
payload.taskId(),
|
||||||
rawWeight, weightUnit, metricWeight, payload.reps()
|
rawWeight, weightUnit, metricWeight, payload.reps()
|
||||||
));
|
));
|
||||||
|
try {
|
||||||
|
cdnClient.uploads.startTask(submission.getVideoProcessingTaskId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to start video processing task for submission " + submission.getId(), e);
|
||||||
|
}
|
||||||
return new SubmissionResponse(submission);
|
return new SubmissionResponse(submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,17 +123,13 @@ public class ExerciseSubmissionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
UploadsClient.FileMetadataResponse metadata = cdnClient.uploads.getFileMetadata(data.videoFileId());
|
var status = cdnClient.uploads.getVideoProcessingTaskStatus(data.taskId());
|
||||||
if (metadata == null) {
|
if (!status.status().equalsIgnoreCase("NOT_STARTED")) {
|
||||||
response.addMessage("Missing video file.");
|
response.addMessage("Invalid video processing task.");
|
||||||
} else if (!metadata.availableForDownload()) {
|
|
||||||
response.addMessage("File not yet available for download.");
|
|
||||||
} else if (!"video/mp4".equals(metadata.mimeType())) {
|
|
||||||
response.addMessage("Invalid video file format.");
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error fetching file metadata.", e);
|
log.error("Error fetching task status.", e);
|
||||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error fetching uploaded video file metadata.");
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error fetching uploaded video task status.");
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ public class SampleDataLoader implements ApplicationListener<ContextRefreshedEve
|
||||||
gen.generate();
|
gen.generate();
|
||||||
completed.add(gen);
|
completed.add(gen);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Generator failed: " + gen.getClass().getSimpleName());
|
throw new RuntimeException("Generator failed: " + gen.getClass().getSimpleName(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
package nl.andrewlalis.gymboard_api.util.sample_data;
|
package nl.andrewlalis.gymboard_api.util.sample_data;
|
||||||
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.ExerciseRepository;
|
import nl.andrewlalis.gymboard_api.domains.api.dao.ExerciseRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.dao.GymRepository;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.dao.submission.SubmissionRepository;
|
import nl.andrewlalis.gymboard_api.domains.api.dao.submission.SubmissionRepository;
|
||||||
|
import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
import nl.andrewlalis.gymboard_api.domains.api.model.Gym;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
|
import nl.andrewlalis.gymboard_api.domains.api.model.WeightUnit;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.Exercise;
|
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.model.submission.Submission;
|
import nl.andrewlalis.gymboard_api.domains.api.model.submission.Submission;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.CdnClient;
|
import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.CdnClient;
|
||||||
import nl.andrewlalis.gymboard_api.domains.api.service.submission.ExerciseSubmissionService;
|
import nl.andrewlalis.gymboard_api.domains.api.service.cdn_client.UploadsClient;
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
|
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
|
||||||
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
|
||||||
import nl.andrewlalis.gymboard_api.util.ULID;
|
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Profile;
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.data.util.Pair;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
@ -25,35 +29,30 @@ import java.util.*;
|
||||||
@Component
|
@Component
|
||||||
@Profile("development")
|
@Profile("development")
|
||||||
public class SampleSubmissionGenerator implements SampleDataGenerator {
|
public class SampleSubmissionGenerator implements SampleDataGenerator {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SampleSubmissionGenerator.class);
|
||||||
|
|
||||||
private final GymRepository gymRepository;
|
private final GymRepository gymRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final ExerciseRepository exerciseRepository;
|
private final ExerciseRepository exerciseRepository;
|
||||||
private final ExerciseSubmissionService submissionService;
|
|
||||||
private final SubmissionRepository submissionRepository;
|
private final SubmissionRepository submissionRepository;
|
||||||
private final ULID ulid;
|
private final ULID ulid;
|
||||||
|
|
||||||
@Value("${app.cdn-origin}")
|
@Value("${app.cdn-origin}")
|
||||||
private String cdnOrigin;
|
private String cdnOrigin;
|
||||||
|
|
||||||
public SampleSubmissionGenerator(GymRepository gymRepository, UserRepository userRepository, ExerciseRepository exerciseRepository, ExerciseSubmissionService submissionService, SubmissionRepository submissionRepository, ULID ulid) {
|
public SampleSubmissionGenerator(GymRepository gymRepository, UserRepository userRepository, ExerciseRepository exerciseRepository, SubmissionRepository submissionRepository, ULID ulid) {
|
||||||
this.gymRepository = gymRepository;
|
this.gymRepository = gymRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.exerciseRepository = exerciseRepository;
|
this.exerciseRepository = exerciseRepository;
|
||||||
this.submissionService = submissionService;
|
|
||||||
this.submissionRepository = submissionRepository;
|
this.submissionRepository = submissionRepository;
|
||||||
this.ulid = ulid;
|
this.ulid = ulid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generate() throws Exception {
|
public void generate() throws Exception {
|
||||||
final CdnClient cdnClient = new CdnClient(cdnOrigin);
|
var uploads = generateUploads();
|
||||||
|
|
||||||
List<String> videoIds = new ArrayList<>();
|
|
||||||
var video1 = cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_curl.mp4"), "video/mp4");
|
|
||||||
var video2 = cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_ohp.mp4"), "video/mp4");
|
|
||||||
videoIds.add(video1.id());
|
|
||||||
videoIds.add(video2.id());
|
|
||||||
|
|
||||||
|
// Now that uploads are complete, we can proceed with generating the submissions.
|
||||||
List<Gym> gyms = gymRepository.findAll();
|
List<Gym> gyms = gymRepository.findAll();
|
||||||
List<User> users = userRepository.findAll();
|
List<User> users = userRepository.findAll();
|
||||||
List<Exercise> exercises = exerciseRepository.findAll();
|
List<Exercise> exercises = exerciseRepository.findAll();
|
||||||
|
@ -65,24 +64,27 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
|
||||||
Random random = new Random(1);
|
Random random = new Random(1);
|
||||||
List<Submission> submissions = new ArrayList<>(count);
|
List<Submission> submissions = new ArrayList<>(count);
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
submissions.add(generateRandomSubmission(
|
Submission submission = generateRandomSubmission(
|
||||||
gyms,
|
gyms,
|
||||||
users,
|
users,
|
||||||
exercises,
|
exercises,
|
||||||
videoIds,
|
uploads,
|
||||||
earliestSubmission,
|
earliestSubmission,
|
||||||
latestSubmission,
|
latestSubmission,
|
||||||
random
|
random
|
||||||
));
|
);
|
||||||
|
submissions.add(submission);
|
||||||
}
|
}
|
||||||
submissionRepository.saveAll(submissions);
|
submissionRepository.saveAll(submissions);
|
||||||
|
|
||||||
|
// After adding all the submissions, we'll signal to CDN that it can start processing.
|
||||||
}
|
}
|
||||||
|
|
||||||
private Submission generateRandomSubmission(
|
private Submission generateRandomSubmission(
|
||||||
List<Gym> gyms,
|
List<Gym> gyms,
|
||||||
List<User> users,
|
List<User> users,
|
||||||
List<Exercise> exercises,
|
List<Exercise> exercises,
|
||||||
List<String> videoIds,
|
Map<Long, Pair<String, String>> uploads,
|
||||||
LocalDateTime earliestSubmission,
|
LocalDateTime earliestSubmission,
|
||||||
LocalDateTime latestSubmission,
|
LocalDateTime latestSubmission,
|
||||||
Random random
|
Random random
|
||||||
|
@ -102,13 +104,16 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
|
||||||
randomChoice(exercises, random),
|
randomChoice(exercises, random),
|
||||||
randomChoice(users, random),
|
randomChoice(users, random),
|
||||||
time,
|
time,
|
||||||
randomChoice(videoIds, random),
|
randomChoice(new ArrayList<>(uploads.keySet()), random),
|
||||||
rawWeight,
|
rawWeight,
|
||||||
weightUnit,
|
weightUnit,
|
||||||
metricWeight,
|
metricWeight,
|
||||||
random.nextInt(13) + 1
|
random.nextInt(13) + 1
|
||||||
);
|
);
|
||||||
submission.setVerified(true);
|
submission.setVerified(true);
|
||||||
|
var uploadData = uploads.get(submission.getVideoProcessingTaskId());
|
||||||
|
submission.setVideoFileId(uploadData.getFirst());
|
||||||
|
submission.setThumbnailFileId(uploadData.getSecond());
|
||||||
return submission;
|
return submission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,4 +130,48 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
|
||||||
Duration dur = Duration.between(start, end);
|
Duration dur = Duration.between(start, end);
|
||||||
return start.plusSeconds(rand.nextLong(dur.toSeconds() + 1));
|
return start.plusSeconds(rand.nextLong(dur.toSeconds() + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a set of sample video uploads to use for all the sample
|
||||||
|
* submissions.
|
||||||
|
* @return A map containing keys representing video processing task ids, and
|
||||||
|
* values being a pair of video and thumbnail file ids.
|
||||||
|
* @throws Exception If an error occurs.
|
||||||
|
*/
|
||||||
|
private Map<Long, Pair<String, String>> generateUploads() throws Exception {
|
||||||
|
final CdnClient cdnClient = new CdnClient(cdnOrigin);
|
||||||
|
|
||||||
|
List<Long> taskIds = new ArrayList<>();
|
||||||
|
taskIds.add(cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_curl.mp4"), "video/mp4"));
|
||||||
|
taskIds.add(cdnClient.uploads.uploadVideo(Path.of("sample_data", "sample_video_ohp.mp4"), "video/mp4"));
|
||||||
|
|
||||||
|
Map<Long, UploadsClient.VideoProcessingTaskStatusResponse> taskStatus = new HashMap<>();
|
||||||
|
for (long taskId : taskIds) {
|
||||||
|
cdnClient.uploads.startTask(taskId);
|
||||||
|
taskStatus.put(taskId, cdnClient.uploads.getVideoProcessingTaskStatus(taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all video uploads to complete.
|
||||||
|
while (
|
||||||
|
taskStatus.values().stream()
|
||||||
|
.map(UploadsClient.VideoProcessingTaskStatusResponse::status)
|
||||||
|
.anyMatch(status -> !List.of("COMPLETED", "FAILED").contains(status.toUpperCase()))
|
||||||
|
) {
|
||||||
|
log.info("Waiting for sample video upload tasks to finish...");
|
||||||
|
Thread.sleep(1000);
|
||||||
|
for (long taskId : taskIds) taskStatus.put(taskId, cdnClient.uploads.getVideoProcessingTaskStatus(taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any upload failed, throw an exception and cancel this generator.
|
||||||
|
if (taskStatus.values().stream().anyMatch(r -> r.status().equalsIgnoreCase("FAILED"))) {
|
||||||
|
throw new IOException("Video upload task processing failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the final data structure.
|
||||||
|
Map<Long, Pair<String, String>> finalResults = new HashMap<>();
|
||||||
|
for (var entry : taskStatus.entrySet()) {
|
||||||
|
finalResults.put(entry.getKey(), Pair.of(entry.getValue().videoFileId(), entry.getValue().thumbnailFileId()));
|
||||||
|
}
|
||||||
|
return finalResults;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,6 +180,20 @@ public class FileStorageService {
|
||||||
Files.deleteIfExists(filePath);
|
Files.deleteIfExists(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void copyTo(String fileId, Path filePath) throws IOException {
|
||||||
|
Path inputFilePath = getStoragePathForFile(fileId);
|
||||||
|
if (Files.notExists(inputFilePath)) {
|
||||||
|
throw new IOException("File " + fileId + " not found.");
|
||||||
|
}
|
||||||
|
try (
|
||||||
|
var in = Files.newInputStream(inputFilePath);
|
||||||
|
var out = Files.newOutputStream(filePath)
|
||||||
|
) {
|
||||||
|
readMetadata(in);
|
||||||
|
in.transferTo(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static LocalDateTime dateFromULID(ULID.Value value) {
|
private static LocalDateTime dateFromULID(ULID.Value value) {
|
||||||
return Instant.ofEpochMilli(value.timestamp())
|
return Instant.ofEpochMilli(value.timestamp())
|
||||||
.atOffset(ZoneOffset.UTC)
|
.atOffset(ZoneOffset.UTC)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package nl.andrewlalis.gymboardcdn.uploads.api;
|
package nl.andrewlalis.gymboardcdn.uploads.api;
|
||||||
|
|
||||||
public record VideoProcessingTaskStatusResponse(
|
public record VideoProcessingTaskStatusResponse(
|
||||||
String status
|
String status,
|
||||||
|
String videoFileId,
|
||||||
|
String thumbnailFileId
|
||||||
) {}
|
) {}
|
||||||
|
|
|
@ -45,6 +45,12 @@ public class VideoProcessingTask {
|
||||||
@Column(nullable = false, updatable = false, length = 26)
|
@Column(nullable = false, updatable = false, length = 26)
|
||||||
private String uploadFileId;
|
private String uploadFileId;
|
||||||
|
|
||||||
|
@Column(length = 26)
|
||||||
|
private String videoFileId;
|
||||||
|
|
||||||
|
@Column(length = 26)
|
||||||
|
private String thumbnailFileId;
|
||||||
|
|
||||||
public VideoProcessingTask() {}
|
public VideoProcessingTask() {}
|
||||||
|
|
||||||
public VideoProcessingTask(Status status, String uploadFileId) {
|
public VideoProcessingTask(Status status, String uploadFileId) {
|
||||||
|
@ -71,4 +77,20 @@ public class VideoProcessingTask {
|
||||||
public String getUploadFileId() {
|
public String getUploadFileId() {
|
||||||
return uploadFileId;
|
return uploadFileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getVideoFileId() {
|
||||||
|
return videoFileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVideoFileId(String videoFileId) {
|
||||||
|
this.videoFileId = videoFileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getThumbnailFileId() {
|
||||||
|
return thumbnailFileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setThumbnailFileId(String thumbnailFileId) {
|
||||||
|
this.thumbnailFileId = thumbnailFileId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,11 @@ public class UploadService {
|
||||||
public VideoProcessingTaskStatusResponse getVideoProcessingStatus(long id) {
|
public VideoProcessingTaskStatusResponse getVideoProcessingStatus(long id) {
|
||||||
VideoProcessingTask task = videoTaskRepository.findById(id)
|
VideoProcessingTask task = videoTaskRepository.findById(id)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
return new VideoProcessingTaskStatusResponse(task.getStatus().name());
|
return new VideoProcessingTaskStatusResponse(
|
||||||
|
task.getStatus().name(),
|
||||||
|
task.getVideoFileId(),
|
||||||
|
task.getThumbnailFileId()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,7 +95,9 @@ public class UploadService {
|
||||||
public void startVideoProcessing(long taskId) {
|
public void startVideoProcessing(long taskId) {
|
||||||
VideoProcessingTask task = videoTaskRepository.findById(taskId)
|
VideoProcessingTask task = videoTaskRepository.findById(taskId)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (task.getStatus() == VideoProcessingTask.Status.NOT_STARTED) {
|
||||||
task.setStatus(VideoProcessingTask.Status.WAITING);
|
task.setStatus(VideoProcessingTask.Status.WAITING);
|
||||||
videoTaskRepository.save(task);
|
videoTaskRepository.save(task);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,20 +89,29 @@ public class VideoProcessingService {
|
||||||
log.info("Started processing task {}.", task.getId());
|
log.info("Started processing task {}.", task.getId());
|
||||||
|
|
||||||
Path uploadFile = fileStorageService.getStoragePathForFile(task.getUploadFileId());
|
Path uploadFile = fileStorageService.getStoragePathForFile(task.getUploadFileId());
|
||||||
|
Path rawUploadFile = uploadFile.resolveSibling(task.getUploadFileId() + "-vid-in");
|
||||||
if (Files.notExists(uploadFile) || !Files.isReadable(uploadFile)) {
|
if (Files.notExists(uploadFile) || !Files.isReadable(uploadFile)) {
|
||||||
log.error("Uploaded video file {} doesn't exist or isn't readable.", uploadFile);
|
log.error("Uploaded video file {} doesn't exist or isn't readable.", uploadFile);
|
||||||
updateTask(task, VideoProcessingTask.Status.FAILED);
|
updateTask(task, VideoProcessingTask.Status.FAILED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
fileStorageService.copyTo(task.getUploadFileId(), rawUploadFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to copy raw video file {} to {}.", uploadFile, rawUploadFile);
|
||||||
|
e.printStackTrace();
|
||||||
|
updateTask(task, VideoProcessingTask.Status.FAILED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Run the actual processing here.
|
// Run the actual processing here.
|
||||||
Path videoFile = uploadFile.resolveSibling(task.getUploadFileId() + "-vid-out");
|
Path videoFile = uploadFile.resolveSibling(task.getUploadFileId() + "-vid-out.mp4");
|
||||||
Path thumbnailFile = uploadFile.resolveSibling(task.getUploadFileId() + "-thm-out");
|
Path thumbnailFile = uploadFile.resolveSibling(task.getUploadFileId() + "-thm-out.jpeg");
|
||||||
try {
|
try {
|
||||||
log.info("Processing video for uploaded video file {}.", uploadFile.getFileName());
|
log.info("Processing video for uploaded video file {}.", uploadFile.getFileName());
|
||||||
videoProcessor.processVideo(uploadFile, videoFile);
|
videoProcessor.processVideo(rawUploadFile, videoFile);
|
||||||
log.info("Generating thumbnail for uploaded video file {}.", uploadFile.getFileName());
|
log.info("Generating thumbnail for uploaded video file {}.", uploadFile.getFileName());
|
||||||
thumbnailGenerator.generateThumbnailImage(uploadFile, thumbnailFile);
|
thumbnailGenerator.generateThumbnailImage(videoFile, thumbnailFile);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
log.error("""
|
log.error("""
|
||||||
|
@ -111,7 +120,7 @@ public class VideoProcessingService {
|
||||||
Output file: {}
|
Output file: {}
|
||||||
Exception message: {}""",
|
Exception message: {}""",
|
||||||
task.getId(),
|
task.getId(),
|
||||||
uploadFile,
|
rawUploadFile,
|
||||||
videoFile,
|
videoFile,
|
||||||
e.getMessage()
|
e.getMessage()
|
||||||
);
|
);
|
||||||
|
@ -129,6 +138,9 @@ public class VideoProcessingService {
|
||||||
// Save the thumbnail too.
|
// Save the thumbnail too.
|
||||||
FileMetadata thumbnailMetadata = new FileMetadata("thumbnail.jpeg", "image/jpeg", true);
|
FileMetadata thumbnailMetadata = new FileMetadata("thumbnail.jpeg", "image/jpeg", true);
|
||||||
String thumbnailFileId = fileStorageService.save(thumbnailFile, thumbnailMetadata);
|
String thumbnailFileId = fileStorageService.save(thumbnailFile, thumbnailMetadata);
|
||||||
|
|
||||||
|
task.setVideoFileId(videoFileId);
|
||||||
|
task.setThumbnailFileId(thumbnailFileId);
|
||||||
updateTask(task, VideoProcessingTask.Status.COMPLETED);
|
updateTask(task, VideoProcessingTask.Status.COMPLETED);
|
||||||
log.info("Finished processing task {}.", task.getId());
|
log.info("Finished processing task {}.", task.getId());
|
||||||
|
|
||||||
|
@ -140,6 +152,7 @@ public class VideoProcessingService {
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
fileStorageService.delete(task.getUploadFileId());
|
fileStorageService.delete(task.getUploadFileId());
|
||||||
|
Files.deleteIfExists(rawUploadFile);
|
||||||
Files.deleteIfExists(videoFile);
|
Files.deleteIfExists(videoFile);
|
||||||
Files.deleteIfExists(thumbnailFile);
|
Files.deleteIfExists(thumbnailFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
Loading…
Reference in New Issue