forked from jsl/video_editing_poc
Handle scheduled clipping failures
This commit is contained in:
parent
020edf9821
commit
4b0be45be0
|
|
@ -27,7 +27,7 @@ Note: the local machine's filesystem root is read-only in this workspace, so abs
|
||||||
7. [x] Add invalid-file handling that logs the reason and moves files to rejected storage.
|
7. [x] Add invalid-file handling that logs the reason and moves files to rejected storage.
|
||||||
8. [x] Add FFmpeg clipping into `/output/clips/<video-name>/` using 8-second MP4/H.264/AAC segments.
|
8. [x] Add FFmpeg clipping into `/output/clips/<video-name>/` using 8-second MP4/H.264/AAC segments.
|
||||||
9. [x] Add success handling that moves processed source files to processed storage.
|
9. [x] Add success handling that moves processed source files to processed storage.
|
||||||
10. [ ] Add failure handling for FFmpeg or filesystem errors.
|
10. [x] Add failure handling for FFmpeg or filesystem errors.
|
||||||
11. [ ] Add unit tests for ordering, invalid-file handling, repeat prevention, and state transitions.
|
11. [ ] Add unit tests for ordering, invalid-file handling, repeat prevention, and state transitions.
|
||||||
12. [ ] Add optional FFmpeg integration test for a short fixture.
|
12. [ ] Add optional FFmpeg integration test for a short fixture.
|
||||||
13. [ ] Add local profile or documented runtime config for enabling the scheduler.
|
13. [ ] Add local profile or documented runtime config for enabling the scheduler.
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,7 @@ public class FolderVideoScanScheduler {
|
||||||
try {
|
try {
|
||||||
findNextCandidate().ifPresentOrElse(
|
findNextCandidate().ifPresentOrElse(
|
||||||
candidate -> {
|
candidate -> {
|
||||||
Path workingFile = moveToWorking(candidate);
|
processCandidate(candidate);
|
||||||
log.info("Claimed folder video candidate for processing: {}", workingFile.getFileName());
|
|
||||||
validateCandidate(workingFile);
|
|
||||||
},
|
},
|
||||||
() -> log.debug("No folder video candidate found in {}", properties.getInputDirectory())
|
() -> log.debug("No folder video candidate found in {}", properties.getInputDirectory())
|
||||||
);
|
);
|
||||||
|
|
@ -59,6 +57,42 @@ public class FolderVideoScanScheduler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void processCandidate(Path candidate) {
|
||||||
|
Path activeFile = candidate;
|
||||||
|
try {
|
||||||
|
activeFile = moveToWorking(candidate);
|
||||||
|
log.info("Claimed folder video candidate for processing: {}", activeFile.getFileName());
|
||||||
|
validateCandidate(activeFile);
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
handleFailure(activeFile, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFailure(Path activeFile, RuntimeException failure) {
|
||||||
|
log.error("Folder video processing failed for {} ({})", activeFile.getFileName(),
|
||||||
|
failure.getClass().getSimpleName());
|
||||||
|
try {
|
||||||
|
Path rejectedFile = moveToDirectory(activeFile, Path.of(properties.getRejectedDirectory()));
|
||||||
|
log.warn("Moved failed folder video source to rejected storage: {}", rejectedFile.getFileName());
|
||||||
|
} catch (RuntimeException rejectionFailure) {
|
||||||
|
markFailed(activeFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void markFailed(Path source) {
|
||||||
|
Path marker = source.resolveSibling(source.getFileName() + ".failed");
|
||||||
|
if (Files.exists(marker)) {
|
||||||
|
log.error("Unable to mark failed folder video source because marker already exists: {}", marker.getFileName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Files.move(source, marker, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
log.warn("Marked folder video source as failed: {}", marker.getFileName());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.error("Unable to quarantine or mark failed folder video source: {}", source.getFileName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void validateCandidate(Path workingFile) {
|
private void validateCandidate(Path workingFile) {
|
||||||
FolderVideoValidator.ValidationResult result = validator.validate(workingFile);
|
FolderVideoValidator.ValidationResult result = validator.validate(workingFile);
|
||||||
if (result.valid()) {
|
if (result.valid()) {
|
||||||
|
|
@ -116,7 +150,8 @@ public class FolderVideoScanScheduler {
|
||||||
return !fileName.startsWith(".")
|
return !fileName.startsWith(".")
|
||||||
&& !fileName.endsWith(".tmp")
|
&& !fileName.endsWith(".tmp")
|
||||||
&& !fileName.endsWith(".part")
|
&& !fileName.endsWith(".part")
|
||||||
&& !fileName.endsWith(".download");
|
&& !fileName.endsWith(".download")
|
||||||
|
&& !fileName.endsWith(".failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Comparator<Path> candidateComparator() {
|
private Comparator<Path> candidateComparator() {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ class FolderVideoScanSchedulerTest {
|
||||||
Files.writeString(tempDir.resolve("upload.mp4.tmp"), "video");
|
Files.writeString(tempDir.resolve("upload.mp4.tmp"), "video");
|
||||||
Files.writeString(tempDir.resolve("upload.mp4.part"), "video");
|
Files.writeString(tempDir.resolve("upload.mp4.part"), "video");
|
||||||
Files.writeString(tempDir.resolve("upload.mp4.download"), "video");
|
Files.writeString(tempDir.resolve("upload.mp4.download"), "video");
|
||||||
|
Files.writeString(tempDir.resolve("upload.mp4.failed"), "video");
|
||||||
Files.writeString(tempDir.resolve("valid.mp4"), "video");
|
Files.writeString(tempDir.resolve("valid.mp4"), "video");
|
||||||
|
|
||||||
Optional<Path> candidate = scheduler(tempDir).findNextCandidate();
|
Optional<Path> candidate = scheduler(tempDir).findNextCandidate();
|
||||||
|
|
@ -143,8 +144,9 @@ class FolderVideoScanSchedulerTest {
|
||||||
FolderVideoValidator validator = mock(FolderVideoValidator.class);
|
FolderVideoValidator validator = mock(FolderVideoValidator.class);
|
||||||
when(validator.validate(any())).thenReturn(FolderVideoValidator.ValidationResult.invalid("invalid"));
|
when(validator.validate(any())).thenReturn(FolderVideoValidator.ValidationResult.invalid("invalid"));
|
||||||
|
|
||||||
assertThrows(IllegalStateException.class, () -> scheduler(tempDir, validator).scan());
|
scheduler(tempDir, validator).scan();
|
||||||
assertTrue(Files.exists(tempDir.resolve("working/invalid.mp4")));
|
|
||||||
|
assertTrue(Files.exists(tempDir.resolve("working/invalid.mp4.failed")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -153,10 +155,58 @@ class FolderVideoScanSchedulerTest {
|
||||||
Files.createDirectory(tempDir.resolve("processed"));
|
Files.createDirectory(tempDir.resolve("processed"));
|
||||||
Files.writeString(tempDir.resolve("processed/1.mp4"), "existing video");
|
Files.writeString(tempDir.resolve("processed/1.mp4"), "existing video");
|
||||||
|
|
||||||
assertThrows(IllegalStateException.class, () -> scheduler(tempDir).scan());
|
scheduler(tempDir).scan();
|
||||||
|
|
||||||
assertEquals("existing video", Files.readString(tempDir.resolve("processed/1.mp4")));
|
assertEquals("existing video", Files.readString(tempDir.resolve("processed/1.mp4")));
|
||||||
assertEquals("new video", Files.readString(tempDir.resolve("working/1.mp4")));
|
assertEquals("new video", Files.readString(tempDir.resolve("rejected/1.mp4")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void quarantinesSourceWhenClipGenerationFails() throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("broken.mp4"), "video");
|
||||||
|
FolderFfmpegClipper clipper = mock(FolderFfmpegClipper.class);
|
||||||
|
when(clipper.clip(any())).thenThrow(new IllegalStateException("ffmpeg failed"));
|
||||||
|
|
||||||
|
scheduler(tempDir, acceptedValidator(), clipper).scan();
|
||||||
|
|
||||||
|
assertEquals("video", Files.readString(tempDir.resolve("rejected/broken.mp4")));
|
||||||
|
assertFalse(Files.exists(tempDir.resolve("working/broken.mp4")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void quarantinesInputWhenClaimingFails() throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("broken.mp4"), "video");
|
||||||
|
Files.createDirectory(tempDir.resolve("working"));
|
||||||
|
Files.writeString(tempDir.resolve("working/broken.mp4"), "collision");
|
||||||
|
|
||||||
|
scheduler(tempDir).scan();
|
||||||
|
|
||||||
|
assertEquals("video", Files.readString(tempDir.resolve("rejected/broken.mp4")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ignoresFailureWhenActiveSourceHasAlreadyDisappeared() throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("vanished.mp4"), "video");
|
||||||
|
FolderVideoValidator validator = mock(FolderVideoValidator.class);
|
||||||
|
when(validator.validate(any())).thenAnswer(invocation -> {
|
||||||
|
Files.delete(invocation.getArgument(0));
|
||||||
|
throw new IllegalStateException("vanished");
|
||||||
|
});
|
||||||
|
|
||||||
|
scheduler(tempDir, validator).scan();
|
||||||
|
|
||||||
|
assertFalse(Files.exists(tempDir.resolve("working/vanished.mp4.failed")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotOverwriteExistingFailureMarker() throws Exception {
|
||||||
|
Path source = Files.writeString(tempDir.resolve("video.mp4"), "source");
|
||||||
|
Files.writeString(tempDir.resolve("video.mp4.failed"), "marker");
|
||||||
|
|
||||||
|
scheduler(tempDir).markFailed(source);
|
||||||
|
|
||||||
|
assertEquals("source", Files.readString(source));
|
||||||
|
assertEquals("marker", Files.readString(tempDir.resolve("video.mp4.failed")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -169,9 +219,7 @@ class FolderVideoScanSchedulerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private FolderVideoScanScheduler scheduler(Path inputDirectory) {
|
private FolderVideoScanScheduler scheduler(Path inputDirectory) {
|
||||||
FolderVideoValidator validator = mock(FolderVideoValidator.class);
|
return scheduler(inputDirectory, acceptedValidator(), successfulClipper());
|
||||||
when(validator.validate(any())).thenReturn(FolderVideoValidator.ValidationResult.accepted());
|
|
||||||
return scheduler(inputDirectory, validator, successfulClipper());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private FolderVideoScanScheduler scheduler(Path inputDirectory, FolderVideoValidator validator) {
|
private FolderVideoScanScheduler scheduler(Path inputDirectory, FolderVideoValidator validator) {
|
||||||
|
|
@ -216,4 +264,10 @@ class FolderVideoScanSchedulerTest {
|
||||||
when(clipper.clip(any())).thenReturn(new FolderFfmpegClipper.ClipResult(tempDir.resolve("output/1"), 1));
|
when(clipper.clip(any())).thenReturn(new FolderFfmpegClipper.ClipResult(tempDir.resolve("output/1"), 1));
|
||||||
return clipper;
|
return clipper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FolderVideoValidator acceptedValidator() {
|
||||||
|
FolderVideoValidator validator = mock(FolderVideoValidator.class);
|
||||||
|
when(validator.validate(any())).thenReturn(FolderVideoValidator.ValidationResult.accepted());
|
||||||
|
return validator;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue