forked from jsl/video_editing_poc
Reject invalid scheduled inputs
This commit is contained in:
parent
146c37b091
commit
cf28898ecf
|
|
@ -24,7 +24,7 @@ Note: the local machine's filesystem root is read-only in this workspace, so abs
|
||||||
4. [x] Add deterministic scanner that picks one candidate video at a time.
|
4. [x] Add deterministic scanner that picks one candidate video at a time.
|
||||||
5. [x] Add repeat-prevention by moving selected inputs to a working folder before processing.
|
5. [x] Add repeat-prevention by moving selected inputs to a working folder before processing.
|
||||||
6. [x] Add valid-video detection using extension filtering and `ffprobe`.
|
6. [x] Add valid-video detection using extension filtering and `ffprobe`.
|
||||||
7. [ ] 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. [ ] Add FFmpeg clipping into `/output/clips/<video-name>/` using 8-second MP4/H.264/AAC segments.
|
8. [ ] Add FFmpeg clipping into `/output/clips/<video-name>/` using 8-second MP4/H.264/AAC segments.
|
||||||
9. [ ] Add success handling that moves processed source files to processed storage.
|
9. [ ] Add success handling that moves processed source files to processed storage.
|
||||||
10. [ ] Add failure handling for FFmpeg or filesystem errors.
|
10. [ ] Add failure handling for FFmpeg or filesystem errors.
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,15 @@ public class FolderVideoScanScheduler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(FolderVideoScanScheduler.class);
|
private static final Logger log = LoggerFactory.getLogger(FolderVideoScanScheduler.class);
|
||||||
|
|
||||||
private final VideoClippingProperties.FolderScheduler properties;
|
private final VideoClippingProperties.FolderScheduler properties;
|
||||||
|
private final FolderVideoValidator validator;
|
||||||
private final AtomicBoolean scanning = new AtomicBoolean(false);
|
private final AtomicBoolean scanning = new AtomicBoolean(false);
|
||||||
|
|
||||||
public FolderVideoScanScheduler(VideoClippingProperties videoClippingProperties) {
|
public FolderVideoScanScheduler(
|
||||||
|
VideoClippingProperties videoClippingProperties,
|
||||||
|
FolderVideoValidator validator
|
||||||
|
) {
|
||||||
this.properties = videoClippingProperties.getFolderScheduler();
|
this.properties = videoClippingProperties.getFolderScheduler();
|
||||||
|
this.validator = validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(fixedDelayString = "${video-clipping.folder-scheduler.poll-interval-ms:5000}")
|
@Scheduled(fixedDelayString = "${video-clipping.folder-scheduler.poll-interval-ms:5000}")
|
||||||
|
|
@ -42,6 +47,7 @@ public class FolderVideoScanScheduler {
|
||||||
candidate -> {
|
candidate -> {
|
||||||
Path workingFile = moveToWorking(candidate);
|
Path workingFile = moveToWorking(candidate);
|
||||||
log.info("Claimed folder video candidate for processing: {}", workingFile.getFileName());
|
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())
|
||||||
);
|
);
|
||||||
|
|
@ -50,13 +56,31 @@ public class FolderVideoScanScheduler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateCandidate(Path workingFile) {
|
||||||
|
FolderVideoValidator.ValidationResult result = validator.validate(workingFile);
|
||||||
|
if (result.valid()) {
|
||||||
|
log.info("Validated folder video candidate: {}", workingFile.getFileName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path rejectedFile = moveToDirectory(workingFile, Path.of(properties.getRejectedDirectory()));
|
||||||
|
log.warn("Rejected invalid folder video candidate {}: {}", rejectedFile.getFileName(), result.reason());
|
||||||
|
}
|
||||||
|
|
||||||
Path moveToWorking(Path candidate) {
|
Path moveToWorking(Path candidate) {
|
||||||
Path workingFile = Path.of(properties.getWorkingDirectory()).resolve(candidate.getFileName());
|
return moveToDirectory(candidate, Path.of(properties.getWorkingDirectory()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path moveToDirectory(Path source, Path targetDirectory) {
|
||||||
|
Path target = targetDirectory.resolve(source.getFileName());
|
||||||
|
if (Files.exists(target)) {
|
||||||
|
throw new IllegalStateException("Refusing to overwrite existing folder video file: " + target);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return Files.move(candidate, workingFile, StandardCopyOption.ATOMIC_MOVE);
|
return Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new IllegalStateException("Unable to move folder video candidate to working directory: "
|
throw new IllegalStateException("Unable to move folder video file " + source.getFileName()
|
||||||
+ candidate.getFileName(), ex);
|
+ " to " + targetDirectory, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
class FolderVideoScanSchedulerTest {
|
class FolderVideoScanSchedulerTest {
|
||||||
|
|
||||||
|
|
@ -118,6 +121,32 @@ class FolderVideoScanSchedulerTest {
|
||||||
assertEquals("video", Files.readString(tempDir.resolve("working/1.mp4")));
|
assertEquals("video", Files.readString(tempDir.resolve("working/1.mp4")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidCandidateIsMovedToRejectedDirectory() throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("invalid.mp4"), "not video");
|
||||||
|
FolderVideoValidator validator = mock(FolderVideoValidator.class);
|
||||||
|
when(validator.validate(any())).thenReturn(
|
||||||
|
FolderVideoValidator.ValidationResult.invalid("ffprobe exited with code 1"));
|
||||||
|
|
||||||
|
scheduler(tempDir, validator).scan();
|
||||||
|
|
||||||
|
assertFalse(Files.exists(tempDir.resolve("invalid.mp4")));
|
||||||
|
assertFalse(Files.exists(tempDir.resolve("working/invalid.mp4")));
|
||||||
|
assertEquals("not video", Files.readString(tempDir.resolve("rejected/invalid.mp4")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void failsWhenInvalidCandidateCannotBeMovedToRejectedDirectory() throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("invalid.mp4"), "not video");
|
||||||
|
Files.createDirectory(tempDir.resolve("rejected"));
|
||||||
|
Files.writeString(tempDir.resolve("rejected/invalid.mp4"), "existing");
|
||||||
|
FolderVideoValidator validator = mock(FolderVideoValidator.class);
|
||||||
|
when(validator.validate(any())).thenReturn(FolderVideoValidator.ValidationResult.invalid("invalid"));
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> scheduler(tempDir, validator).scan());
|
||||||
|
assertTrue(Files.exists(tempDir.resolve("working/invalid.mp4")));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void failsWhenCandidateCannotBeMovedToWorkingDirectory() throws Exception {
|
void failsWhenCandidateCannotBeMovedToWorkingDirectory() throws Exception {
|
||||||
Path candidate = Files.writeString(tempDir.resolve("1.mp4"), "video");
|
Path candidate = Files.writeString(tempDir.resolve("1.mp4"), "video");
|
||||||
|
|
@ -128,6 +157,12 @@ class FolderVideoScanSchedulerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private FolderVideoScanScheduler scheduler(Path inputDirectory) {
|
private FolderVideoScanScheduler scheduler(Path inputDirectory) {
|
||||||
|
FolderVideoValidator validator = mock(FolderVideoValidator.class);
|
||||||
|
when(validator.validate(any())).thenReturn(FolderVideoValidator.ValidationResult.accepted());
|
||||||
|
return scheduler(inputDirectory, validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FolderVideoScanScheduler scheduler(Path inputDirectory, FolderVideoValidator validator) {
|
||||||
VideoClippingProperties properties = new VideoClippingProperties();
|
VideoClippingProperties properties = new VideoClippingProperties();
|
||||||
properties.getFolderScheduler().setInputDirectory(inputDirectory.toString());
|
properties.getFolderScheduler().setInputDirectory(inputDirectory.toString());
|
||||||
Path workingDirectory = tempDir.resolve("working");
|
Path workingDirectory = tempDir.resolve("working");
|
||||||
|
|
@ -139,6 +174,13 @@ class FolderVideoScanSchedulerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
properties.getFolderScheduler().setWorkingDirectory(workingDirectory.toString());
|
properties.getFolderScheduler().setWorkingDirectory(workingDirectory.toString());
|
||||||
return new FolderVideoScanScheduler(properties);
|
Path rejectedDirectory = tempDir.resolve("rejected");
|
||||||
|
try {
|
||||||
|
Files.createDirectories(rejectedDirectory);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
properties.getFolderScheduler().setRejectedDirectory(rejectedDirectory.toString());
|
||||||
|
return new FolderVideoScanScheduler(properties, validator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue