Implement job metadata extraction and task management features. Add validation for frame range limits, enhance job and task data structures, and introduce new API endpoints for metadata and task retrieval. Update client-side components to handle metadata extraction and display task statuses. Improve error handling in API responses.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { jobs } from '../utils/api';
|
||||
|
||||
export default function JobSubmission({ onSuccess }) {
|
||||
@@ -12,6 +12,113 @@ export default function JobSubmission({ onSuccess }) {
|
||||
const [file, setFile] = useState(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [metadataStatus, setMetadataStatus] = useState(null); // 'extracting', 'completed', 'error'
|
||||
const [metadata, setMetadata] = useState(null);
|
||||
const [currentJobId, setCurrentJobId] = useState(null);
|
||||
|
||||
// Poll for metadata after file upload
|
||||
useEffect(() => {
|
||||
if (!currentJobId || metadataStatus !== 'extracting') return;
|
||||
|
||||
let pollCount = 0;
|
||||
const maxPolls = 30; // 60 seconds max (30 * 2 seconds)
|
||||
let timeoutId = null;
|
||||
|
||||
const pollMetadata = async () => {
|
||||
pollCount++;
|
||||
|
||||
// Stop polling after timeout
|
||||
if (pollCount > maxPolls) {
|
||||
setMetadataStatus('error');
|
||||
// Cancel temp job on timeout
|
||||
try {
|
||||
await jobs.cancel(currentJobId);
|
||||
} catch (err) {
|
||||
// Ignore errors when canceling
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const metadata = await jobs.getMetadata(currentJobId);
|
||||
if (metadata) {
|
||||
setMetadata(metadata);
|
||||
setMetadataStatus('completed');
|
||||
// Auto-populate form fields
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
frame_start: metadata.frame_start || prev.frame_start,
|
||||
frame_end: metadata.frame_end || prev.frame_end,
|
||||
output_format: metadata.render_settings?.output_format || prev.output_format,
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
// Metadata not ready yet, continue polling (only if 404/not found)
|
||||
if (err.message.includes('404') || err.message.includes('not found')) {
|
||||
// Continue polling via interval
|
||||
} else {
|
||||
setMetadataStatus('error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(pollMetadata, 2000);
|
||||
|
||||
// Set timeout to stop polling after 60 seconds
|
||||
timeoutId = setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
if (metadataStatus === 'extracting') {
|
||||
setMetadataStatus('error');
|
||||
// Cancel temp job on timeout
|
||||
jobs.cancel(currentJobId).catch(() => {});
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
// Cleanup: cancel temp job if component unmounts during extraction
|
||||
if (currentJobId && metadataStatus === 'extracting') {
|
||||
jobs.cancel(currentJobId).catch(() => {});
|
||||
}
|
||||
};
|
||||
}, [currentJobId, metadataStatus]);
|
||||
|
||||
const handleFileChange = async (e) => {
|
||||
const selectedFile = e.target.files[0];
|
||||
if (!selectedFile) {
|
||||
setFile(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
setMetadataStatus(null);
|
||||
setMetadata(null);
|
||||
setCurrentJobId(null);
|
||||
|
||||
// If it's a blend file, create a temporary job to extract metadata
|
||||
if (selectedFile.name.toLowerCase().endsWith('.blend')) {
|
||||
try {
|
||||
// Create a temporary job for metadata extraction
|
||||
const tempJob = await jobs.create({
|
||||
name: 'Metadata Extraction',
|
||||
frame_start: 1,
|
||||
frame_end: 10,
|
||||
output_format: 'PNG',
|
||||
allow_parallel_runners: true,
|
||||
});
|
||||
|
||||
setCurrentJobId(tempJob.id);
|
||||
setMetadataStatus('extracting');
|
||||
|
||||
// Upload file to trigger metadata extraction
|
||||
await jobs.uploadFile(tempJob.id, selectedFile);
|
||||
} catch (err) {
|
||||
console.error('Failed to start metadata extraction:', err);
|
||||
setMetadataStatus('error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -27,7 +134,16 @@ export default function JobSubmission({ onSuccess }) {
|
||||
throw new Error('Invalid frame range');
|
||||
}
|
||||
|
||||
// Create job
|
||||
// If we have a temporary job for metadata extraction, cancel it
|
||||
if (currentJobId) {
|
||||
try {
|
||||
await jobs.cancel(currentJobId);
|
||||
} catch (err) {
|
||||
// Ignore errors when canceling temp job
|
||||
}
|
||||
}
|
||||
|
||||
// Create actual job
|
||||
const job = await jobs.create({
|
||||
name: formData.name,
|
||||
frame_start: parseInt(formData.frame_start),
|
||||
@@ -48,6 +164,9 @@ export default function JobSubmission({ onSuccess }) {
|
||||
allow_parallel_runners: true,
|
||||
});
|
||||
setFile(null);
|
||||
setMetadata(null);
|
||||
setMetadataStatus(null);
|
||||
setCurrentJobId(null);
|
||||
e.target.reset();
|
||||
|
||||
if (onSuccess) {
|
||||
@@ -150,10 +269,37 @@ export default function JobSubmission({ onSuccess }) {
|
||||
<input
|
||||
type="file"
|
||||
accept=".blend"
|
||||
onChange={(e) => setFile(e.target.files[0])}
|
||||
onChange={handleFileChange}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-600 focus:border-transparent file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-purple-50 file:text-purple-700 hover:file:bg-purple-100"
|
||||
/>
|
||||
{metadataStatus === 'extracting' && (
|
||||
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg text-blue-700 text-sm">
|
||||
Extracting metadata from blend file...
|
||||
</div>
|
||||
)}
|
||||
{metadataStatus === 'completed' && metadata && (
|
||||
<div className="mt-2 p-3 bg-green-50 border border-green-200 rounded-lg text-sm">
|
||||
<div className="text-green-700 font-semibold mb-1">Metadata extracted successfully!</div>
|
||||
<div className="text-green-600 text-xs space-y-1">
|
||||
<div>Frames: {metadata.frame_start} - {metadata.frame_end}</div>
|
||||
<div>Resolution: {metadata.render_settings?.resolution_x} x {metadata.render_settings?.resolution_y}</div>
|
||||
<div>Engine: {metadata.render_settings?.engine}</div>
|
||||
<div>Samples: {metadata.render_settings?.samples}</div>
|
||||
<div className="text-gray-600 mt-2">Form fields have been auto-populated. You can adjust them if needed.</div>
|
||||
{(formData.frame_start < metadata.frame_start || formData.frame_end > metadata.frame_end) && (
|
||||
<div className="mt-2 p-2 bg-yellow-50 border border-yellow-200 rounded text-yellow-800 text-xs">
|
||||
<strong>Warning:</strong> Your frame range ({formData.frame_start}-{formData.frame_end}) exceeds the blend file's range ({metadata.frame_start}-{metadata.frame_end}). This may cause errors.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{metadataStatus === 'error' && (
|
||||
<div className="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-yellow-700 text-sm">
|
||||
Could not extract metadata. Please fill in the form manually.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user