package utils
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"skraak_mcp/db"
)
type AudioMothData struct {
Timestamp time.Time
RecorderID string
Gain db.GainLevel
BatteryV float64
TempC float64
}
var (
audiomothPattern = regexp.MustCompile(`(?i)AudioMoth`)
structuredPattern = regexp.MustCompile(
`Recorded at (\d{2}:\d{2}:\d{2}) (\d{2}/\d{2}/\d{4}) \(UTC([+-]\d+)\) by AudioMoth ([A-F0-9]+) at ([\w-]+) gain while battery was ([\d.]+)V and temperature was ([-\d.]+)C`,
)
)
func IsAudioMoth(comment, artist string) bool {
return audiomothPattern.MatchString(comment) || audiomothPattern.MatchString(artist)
}
func ParseAudioMothComment(comment string) (*AudioMothData, error) {
if data, err := parseStructuredComment(comment); err == nil {
return data, nil
}
return parseLegacyComment(comment)
}
func parseStructuredComment(comment string) (*AudioMothData, error) {
matches := structuredPattern.FindStringSubmatch(comment)
if matches == nil {
return nil, fmt.Errorf("comment does not match structured AudioMoth format")
}
timeStr := matches[1] dateStr := matches[2] timezoneStr := matches[3] recorderID := matches[4] gainStr := matches[5] batteryStr := matches[6] tempStr := matches[7]
timestamp, err := parseAudioMothTimestamp(timeStr, dateStr, timezoneStr)
if err != nil {
return nil, fmt.Errorf("failed to parse timestamp: %w", err)
}
gain, err := parseGainLevel(gainStr)
if err != nil {
return nil, fmt.Errorf("failed to parse gain: %w", err)
}
batteryV, err := strconv.ParseFloat(batteryStr, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse battery voltage: %w", err)
}
tempC, err := strconv.ParseFloat(tempStr, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse temperature: %w", err)
}
return &AudioMothData{
Timestamp: timestamp,
RecorderID: recorderID,
Gain: gain,
BatteryV: batteryV,
TempC: tempC,
}, nil
}
func parseLegacyComment(comment string) (*AudioMothData, error) {
parts := strings.Fields(comment)
if len(parts) < 10 {
return nil, fmt.Errorf("comment has insufficient parts (got %d, need at least 10)", len(parts))
}
timeStr := parts[2]
dateStr := parts[3]
timezoneStr := strings.Trim(parts[4], "()")
recorderID := parts[7]
gainStr := parts[9]
timestamp, err := parseAudioMothTimestamp(timeStr, dateStr, timezoneStr)
if err != nil {
return nil, fmt.Errorf("failed to parse timestamp: %w", err)
}
gain, err := parseGainLevel(gainStr)
if err != nil {
return nil, fmt.Errorf("failed to parse gain: %w", err)
}
batteryStr := parts[len(parts)-5]
batteryStr = strings.TrimSuffix(batteryStr, "V")
batteryV, err := strconv.ParseFloat(batteryStr, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse battery voltage: %w", err)
}
tempStr := parts[len(parts)-1]
tempStr = strings.TrimSuffix(tempStr, ".")
tempStr = strings.TrimSuffix(tempStr, "C")
tempC, err := strconv.ParseFloat(tempStr, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse temperature: %w", err)
}
return &AudioMothData{
Timestamp: timestamp,
RecorderID: recorderID,
Gain: gain,
BatteryV: batteryV,
TempC: tempC,
}, nil
}
func parseAudioMothTimestamp(timeStr, dateStr, timezoneStr string) (time.Time, error) {
timeParts := strings.Split(timeStr, ":")
if len(timeParts) != 3 {
return time.Time{}, fmt.Errorf("invalid time format: %s", timeStr)
}
hour, _ := strconv.Atoi(timeParts[0])
minute, _ := strconv.Atoi(timeParts[1])
second, _ := strconv.Atoi(timeParts[2])
dateParts := strings.Split(dateStr, "/")
if len(dateParts) != 3 {
return time.Time{}, fmt.Errorf("invalid date format: %s", dateStr)
}
day, _ := strconv.Atoi(dateParts[0])
month, _ := strconv.Atoi(dateParts[1])
year, _ := strconv.Atoi(dateParts[2])
timezoneStr = strings.TrimPrefix(timezoneStr, "UTC")
offsetHours, err := strconv.Atoi(timezoneStr)
if err != nil {
return time.Time{}, fmt.Errorf("invalid timezone offset: %s", timezoneStr)
}
offsetSeconds := offsetHours * 3600
loc := time.FixedZone(fmt.Sprintf("UTC%+d", offsetHours), offsetSeconds)
timestamp := time.Date(year, time.Month(month), day, hour, minute, second, 0, loc)
return timestamp, nil
}
func parseGainLevel(gainStr string) (db.GainLevel, error) {
gainStr = strings.ToLower(strings.TrimSpace(gainStr))
switch gainStr {
case "low":
return db.GainLow, nil
case "low-medium":
return db.GainLowMedium, nil
case "medium":
return db.GainMedium, nil
case "medium-high":
return db.GainMediumHigh, nil
case "high":
return db.GainHigh, nil
default:
return "", fmt.Errorf("unknown gain level: %s", gainStr)
}
}