file_import.go
package wav
import (
"fmt"
"path/filepath"
"time"
"skraak/astro"
"skraak/utils"
)
// TimestampResult holds the result of timestamp resolution for a single file
type TimestampResult struct {
Timestamp time.Time
IsAudioMoth bool
MothData *AudioMothData
}
// ResolveTimestamp resolves a file's timestamp using the standard priority chain:
// 1. AudioMoth comment parsing
// 2. Filename timestamp parsing + timezone offset
// 3. File modification time (if useFileModTime is true)
//
// Returns an error if no timestamp could be determined.
func ResolveTimestamp(wavMeta *WAVMetadata, filePath string, timezoneID string, useFileModTime bool, preParsedFilenameTime *time.Time) (*TimestampResult, error) {
result := &TimestampResult{}
// Step 1: Try AudioMoth comment
if IsAudioMoth(wavMeta.Comment, wavMeta.Artist) {
result.IsAudioMoth = true
mothData, err := ParseAudioMothComment(wavMeta.Comment)
if err == nil {
result.MothData = mothData
result.Timestamp = mothData.Timestamp
return result, nil
}
// AudioMoth detected but parsing failed — fall through to filename
}
// Step 2: Try filename timestamp
if preParsedFilenameTime != nil && !preParsedFilenameTime.IsZero() {
result.Timestamp = *preParsedFilenameTime
return result, nil
} else if HasTimestampFilename(filePath) {
filenameTimestamps, err := ParseFilenameTimestamps([]string{filepath.Base(filePath)})
if err == nil {
adjustedTimestamps, err := ApplyTimezoneOffset(filenameTimestamps, timezoneID)
if err == nil && len(adjustedTimestamps) > 0 {
result.Timestamp = adjustedTimestamps[0]
return result, nil
}
}
}
// Step 3: File modification time fallback (optional)
if useFileModTime && !wavMeta.FileModTime.IsZero() {
result.Timestamp = wavMeta.FileModTime
return result, nil
}
return nil, fmt.Errorf("cannot resolve timestamp (no AudioMoth, filename pattern, or file modification time)")
}
// FileProcessingResult holds all extracted metadata for a single file
type FileProcessingResult struct {
FileName string
Hash string
Duration float64
SampleRate int
TimestampLocal time.Time
IsAudioMoth bool
MothData *AudioMothData
AstroData astro.AstronomicalData
}
// ProcessSingleFile runs the full single-file processing pipeline:
// WAV header parsing → XXH64 hash → timestamp resolution → astronomical data
//
// Set useFileModTime to true to allow file modification time as a timestamp fallback.
func ProcessSingleFile(filePath string, latitude, longitude float64, timezoneID string, useFileModTime bool) (*FileProcessingResult, error) {
// Step 1: Parse WAV header
metadata, err := ParseWAVHeader(filePath)
if err != nil {
return nil, fmt.Errorf("WAV header parsing failed: %w", err)
}
// Step 2: Calculate hash
hash, err := utils.ComputeXXH64(filePath)
if err != nil {
return nil, fmt.Errorf("hash calculation failed: %w", err)
}
// Step 3: Resolve timestamp
tsResult, err := ResolveTimestamp(metadata, filePath, timezoneID, useFileModTime, nil)
if err != nil {
return nil, err
}
// Step 4: Calculate astronomical data
astroData := astro.CalculateAstronomicalData(
tsResult.Timestamp.UTC(),
metadata.Duration,
latitude,
longitude,
)
return &FileProcessingResult{
FileName: filepath.Base(filePath),
Hash: hash,
Duration: metadata.Duration,
SampleRate: metadata.SampleRate,
TimestampLocal: tsResult.Timestamp,
IsAudioMoth: tsResult.IsAudioMoth,
MothData: tsResult.MothData,
AstroData: astroData,
}, nil
}