WKQ7LFTPDGWTPJKRWB6DH5PUCX2HF34UCGJDIPYC5PTDX4MCZJXAC RUVJ3V4N5V4Z3HSH2YYESKQF5G7RIHBFB5TLV2IPDWXSGJDRD54AC KZKLAINJJWZ64T5MUZT34LJVQIKBTKZ6EJGD7C7TTSSDGCHEDPMAC 2HAQZPV377VV26SMPSXSZR6CL7SS2GTNPR5COIAPN47NLJILRQGAC QVIGQOQZIEXLFMMAA7RTL7MQWI4MC3CH22R6YO6J7LGLHWLCSD4AC LBWQJEDHCNUNMEJWXILGBGYZUKQI7CDAMH2BD44HULM77SVH5UYQC GPQSOVBPY7VTPHD75R6VWSNITPOL3AECF4DHJB32MF5Z72NV7YMQC JAT3DXOLENZZGXE2NYFF3TVQAQIXMMNYO234ETKQGC2CRHJVZERQC // ParseWAVHeader efficiently reads only the WAV file header to extract metadata.// It reads the first 200KB of the file, which should be sufficient for all header chunks.// ParseWAVHeader extracts metadata from WAV file including duration, sample rate, and INFO chunksfunc ParseWAVHeader(filepath string) (*WAVMetadata, error) {
// readAndParseHeader opens a WAV file, reads its header using the provided buffer pool,// parses metadata, and sets file modification time and size.func readAndParseHeader(filepath string, getBuf func() *[]byte, putBuf func(*[]byte)) (*WAVMetadata, error) {
}// ParseWAVHeader efficiently reads only the WAV file header to extract metadata.// It reads the first 200KB of the file, which should be sufficient for all header chunks.func ParseWAVHeader(filepath string) (*WAVMetadata, error) {return readAndParseHeader(filepath, getHeaderBuffer, putHeaderBuffer)
file, err := os.Open(filepath)if err != nil {return 0, 0, fmt.Errorf("failed to open file: %w", err)}defer func() { _ = file.Close() }()// Get minimal header buffer from pool (4KB)headerBufPtr := getMinimalHeaderBuffer()defer putMinimalHeaderBuffer(headerBufPtr)headerBuf := (*headerBufPtr)[:cap(*headerBufPtr)]// Read first 4KB - sufficient for fmt + data chunk headers in 99% of filesn, err := file.Read(headerBuf)if err != nil && err != io.EOF {return 0, 0, fmt.Errorf("failed to read header: %w", err)}headerBuf = headerBuf[:n]// Parse minimal metadatasampleRate, duration, err = parseWAVMinimal(headerBuf)
metadata, err := readAndParseHeader(filepath, getMinimalHeaderBuffer, putMinimalHeaderBuffer)
return sampleRate, duration, nil}// parseWAVMinimal parses only essential WAV metadata from a byte buffer.// Returns (sampleRate, duration, error). Delegates to parseWAVFromBytes and// extracts just the fields needed for batch processing.func parseWAVMinimal(data []byte) (sampleRate int, duration float64, err error) {metadata, err := parseWAVFromBytes(data)if err != nil {return 0, 0, err}
// ImportStage identifies the pipeline stage where an error occurred.type ImportStage stringconst (StageScan ImportStage = "scan" // directory scanningStageHash ImportStage = "hash" // hash computationStageParse ImportStage = "parse" // WAV header / filename parsingStageProcess ImportStage = "process" // file processingStageValidation ImportStage = "validation" // validation checksStageInsert ImportStage = "insert" // database insertionStageImport ImportStage = "import" // database import (segment pipeline))
FileName string `json:"file_name"`Error string `json:"error"`Stage string `json:"stage"` // "scan", "hash", "parse", "validate", "insert"
FileName string `json:"file_name"`Error string `json:"error"`Stage ImportStage `json:"stage"`
// fileData holds all data for a single file to be importedtype fileData struct {FileName stringHash stringDuration float64SampleRate intTimestampLocal time.TimeIsAudioMoth boolMothData *AudioMothDataAstroData AstronomicalData}
// FileProcessingResult is used for both single-file and cluster import pipelines.
func batchProcessFiles(wavFiles []string, location *LocationData) ([]*fileData, []FileImportError) {var filesData []*fileData
func batchProcessFiles(wavFiles []string, location *LocationData) ([]*FileProcessingResult, []FileImportError) {var filesData []*FileProcessingResult
File string `json:"file,omitempty"`Stage string `json:"stage"` // "validation", "hash", "import"Message string `json:"message"`
File string `json:"file,omitempty"`Stage utils.ImportStage `json:"stage"`Message string `json:"message"`
## [2026-05-05] Utils refactoring: dedup types, fix error wrapping, consolidate WAV parsers, typed ImportStageFour focused refactoring changes in utils/ and tools/:- **Merged `fileData` into `FileProcessingResult`**: The two identical structs(8 fields, same order, same types) in `cluster_import.go` and `file_import.go`have been unified. `cluster_import.go` now uses the exported`FileProcessingResult` everywhere. Removes a type, reduces mental overhead.
- **Fixed `%v` → `%w` in `insertSingleFile`**: Five `fmt.Errorf` calls in`cluster_import.go::insertSingleFile` used `%v` instead of `%w`, breaking`errors.Is`/`errors.As` chains. Now consistent with the rest of the file.- **Consolidated `ParseWAVHeader*` boilerplate**: Extracted `readAndParseHeader`helper that handles open→stat→read→parse→set-modtime. `ParseWAVHeader` and`ParseWAVHeaderMinimal` are now thin wrappers (2-4 lines each).`ParseWAVHeaderWithHash` keeps its own open+hash logic (needs the file handlefor streaming). Removed now-unused `parseWAVMinimal`.- **Typed `ImportStage` constants**: Defined `ImportStage` type with constants(`StageScan`, `StageHash`, `StageParse`, `StageProcess`, `StageValidation`,`StageInsert`, `StageImport`) in `file_import.go`. Both `FileImportError`and `ImportSegmentError` now use `ImportStage` instead of `string`.All usages in `cluster_import.go`, `import_segments.go`, and`import_unstructured.go` updated. Eliminates typos and clarifies the stageset in one place.