KNQ2D44EXRDVEF452BX32KPW4SBRLCX2KMNL6UHDYBED7JBXTU6QC // First, identify AudioMoth vs non-AudioMoth files and batch parse filenames if neededconsole.log('Identifying file types and parsing filenames...');const nonAudioMothFiles: string[] = [];const fileTypeMap = new Map<string, boolean>(); // filename -> isAudioMoth
// Step 1: Get list of audio files (already done - scannedFiles)console.log(`Processing ${scannedFiles.length} audio files`);
// Quick scan to identify file typesfor (const fileHandle of scannedFiles) {const file = await fileHandle.getFile();try {const metadata = await extractAudioMetadata(file);const isAudioMoth = isAudioMothFile({artist: metadata.artist,comment: metadata.comment,});fileTypeMap.set(file.name, isAudioMoth);if (!isAudioMoth) {nonAudioMothFiles.push(file.name);}} catch (error) {console.warn(`Could not determine file type for ${file.name}:`, error);// Assume non-AudioMoth if we can't determinefileTypeMap.set(file.name, false);nonAudioMothFiles.push(file.name);}}
// Step 2: Check first file to determine file type for entire clusterconsole.log('Determining file type from first file...');const firstFileHandle = scannedFiles[0];const firstFile = await firstFileHandle.getFile();
// Batch parse non-AudioMoth filenames if we have any and timezone is availableconst timestampMap = new Map<string, FilenameParsing>();if (nonAudioMothFiles.length > 0 && clusterTimezone) {try {console.log(`Batch parsing ${nonAudioMothFiles.length} non-AudioMoth filenames with timezone ${clusterTimezone}`);const parsedTimestamps = parseClusterFilenames(nonAudioMothFiles, clusterTimezone);// Create lookup mapparsedTimestamps.forEach(parsed => {timestampMap.set(parsed.fileName, parsed);});console.log(`Successfully parsed timestamps for ${parsedTimestamps.length} files`);} catch (error) {console.error('Error parsing filenames:', error);console.warn('Continuing without filename timestamps - files will use current time');
let isAudioMothCluster = false;let timestampMap = new Map<string, FilenameParsing>();try {const firstFileMetadata = await extractAudioMetadata(firstFile);isAudioMothCluster = isAudioMothFile({artist: firstFileMetadata.artist,comment: firstFileMetadata.comment,});console.log(`Cluster determined as: ${isAudioMothCluster ? 'AudioMoth' : 'Non-AudioMoth'} files`);// Step 3: Process according to cluster typeif (!isAudioMothCluster) {// Non-AudioMoth: batch parse all filenames for timestampsif (clusterTimezone) {console.log('Batch parsing filenames for non-AudioMoth cluster...');const allFilenames = scannedFiles.map(handle => handle.name);const parsedTimestamps = parseClusterFilenames(allFilenames, clusterTimezone);// Create lookup mapparsedTimestamps.forEach(parsed => {timestampMap.set(parsed.fileName, parsed);});console.log(`Successfully parsed timestamps for ${parsedTimestamps.length} files`);// Log timestamps for verificationconsole.log('Parsed timestamps:');parsedTimestamps.slice(0, 10).forEach((parsed, index) => {console.log(`${index + 1}. ${parsed.fileName}:`);console.log(` Local: ${parsed.timestamp.timestampLocal}`);console.log(` UTC: ${parsed.timestamp.timestampUTC}`);});if (parsedTimestamps.length > 10) {console.log(`... and ${parsedTimestamps.length - 10} more files`);}} else {console.warn('No cluster timezone available for non-AudioMoth files');}
} else if (nonAudioMothFiles.length > 0) {console.warn(`Found ${nonAudioMothFiles.length} non-AudioMoth files but no cluster timezone available`);
} catch (error) {console.error('Error determining file type from first file:', error);throw new Error('Failed to determine file type - cannot proceed with import');
// Extract metadata from the audio fileconsole.log(`Processing metadata for file ${i + 1}/${scannedFiles.length}:`, file.name);// Get precomputed timestamp for non-AudioMoth filesconst precomputedTimestamp = timestampMap.get(file.name)?.timestamp;// Use the locationId from props and pass precomputed timestampconst fileImportData = await createFileImportData(file, locationId, clusterId, precomputedTimestamp);processedFileData.push(fileImportData);
console.log(`Processing file ${i + 1}/${scannedFiles.length}: ${file.name}`);
console.log(`Processed metadata for ${file.name}:`, fileImportData);
if (isAudioMothCluster) {// AudioMoth files: verify this file is actually AudioMothconst metadata = await extractAudioMetadata(file);const fileIsAudioMoth = isAudioMothFile({artist: metadata.artist,comment: metadata.comment,});if (!fileIsAudioMoth) {throw new Error(`Expected AudioMoth file but found non-AudioMoth: ${file.name}`);}// Process as AudioMoth fileconst fileImportData = await createFileImportData(file, locationId, clusterId);processedFileData.push(fileImportData);} else {// Non-AudioMoth files: verify this file is not AudioMothconst metadata = await extractAudioMetadata(file);const fileIsAudioMoth = isAudioMothFile({artist: metadata.artist,comment: metadata.comment,});if (fileIsAudioMoth) {throw new Error(`Expected non-AudioMoth file but found AudioMoth: ${file.name}`);}// Process as non-AudioMoth file with precomputed timestampconst precomputedTimestamp = timestampMap.get(file.name)?.timestamp;const fileImportData = await createFileImportData(file, locationId, clusterId, precomputedTimestamp);processedFileData.push(fileImportData);}
console.error(`Error processing metadata for ${file.name}:`, error);// Continue with next file
console.error(`Error processing file ${file.name}:`, error);// For assertion failures, we should stop the entire processif (error instanceof Error && error.message.includes('Expected')) {throw error; // Re-throw assertion errors to stop processing}// For other errors, continue with next file
// TODO: Call the file import API here with datasetId and processedFileDataconsole.log('Ready to import files to dataset:', datasetId, processedFileData);console.log('Processed files stored:', processedFiles);
// All files processed successfully, now update database in one transactionconsole.log(`Sending ${processedFileData.length} files to database...`);
// CompletesetImportState('completed');setTimeout(() => {onImportComplete();handleClose();}, 1000);
try {if (!isAuthenticated) {throw new Error('Not authenticated');}const accessToken = await getAccessToken();const importPayload = {files: processedFileData,datasetId: datasetId};console.log('Import payload:', {fileCount: importPayload.files.length,datasetId: importPayload.datasetId,sampleFile: importPayload.files[0] // Log first file as sample});const response = await fetch('/api/file-import', {method: 'POST',headers: {'Content-Type': 'application/json','Authorization': `Bearer ${accessToken}`,},body: JSON.stringify(importPayload),});if (!response.ok) {const errorData = await response.json();throw new Error(`HTTP ${response.status}: ${errorData.message || errorData.error || 'Unknown error'}`);}const result = await response.json();console.log('Database import successful:', result);setImportState('completed');// Show success for a moment, then closesetTimeout(() => {onImportComplete();handleClose();}, 2000);} catch (dbError) {console.error('Database import failed:', dbError);setImportState('cancelled'); // Use cancelled state to show error// TODO: Better error handling - maybe show error state in UIthrow dbError;}
}, [selectedFolder, folderPath, scanAudioFiles, onImportComplete, handleClose, clusterId, datasetId, locationId, processedFiles, clusterTimezone]);
}, [selectedFolder, folderPath, scanAudioFiles, onImportComplete, handleClose, clusterId, datasetId, locationId, processedFiles, clusterTimezone, isAuthenticated, getAccessToken]);