isnight.go
package cmd
import (
"encoding/json"
"flag"
"fmt"
"os"
"skraak/tools/calls"
)
// RunIsNight handles the "isnight" subcommand
//
// JSON output schema (full):
//
// {
// "file_path": string, // Path to the WAV file
// "timestamp_utc": string, // Recording start timestamp (UTC)
// "solar_night": bool, // True if recorded during solar night
// "civil_night": bool, // True if recorded during civil night
// "diurnal_active": bool, // True if during diurnal active period
// "moon_phase": float, // Moon phase (0.0=new, 1.0=full)
// "duration_seconds": float, // Recording duration in seconds
// "timestamp_source": string, // How timestamp was derived (comment/filename/mtime)
// "midpoint_utc": string, // Recording midpoint timestamp (UTC)
// "sunrise_utc": string, // Sunrise time (UTC), omitted if not applicable
// "sunset_utc": string, // Sunset time (UTC), omitted if not applicable
// "dawn_utc": string, // Civil dawn time (UTC), omitted if not applicable
// "dusk_utc": string // Civil dusk time (UTC), omitted if not applicable
// }
//
// JSON output schema (--brief):
//
// {
// "file_path": string, // Path to the WAV file
// "solar_night": bool // True if recorded during solar night
// }
func RunIsNight(args []string) error {
fs := flag.NewFlagSet("isnight", flag.ExitOnError)
filePath := fs.String("file", "", "Path to WAV file (required)")
lat := fs.Float64("lat", 0, "Latitude in decimal degrees (required)")
lng := fs.Float64("lng", 0, "Longitude in decimal degrees (required)")
timezone := fs.String("timezone", "UTC", "IANA timezone ID for filename timestamps (e.g. Pacific/Auckland)")
brief := fs.Bool("brief", false, "Output only file_path and solar_night (saves tokens for batch use)")
fs.Usage = usagePrinter(fs,
"skraak isnight --file <path> --lat <lat> --lng <lng> [--timezone <tz>] [--brief]",
"Determine if a WAV file was recorded at night based on file metadata and GPS coordinates.\n\n"+
"Uses the recording midpoint (not start time) for astronomical calculations.\n"+
"Timestamp resolution: AudioMoth comment → filename → file modification time.",
"skraak isnight --file recording.wav --lat -36.85 --lng 174.76",
"skraak isnight --file recording.wav --lat -36.85 --lng 174.76 --timezone Pacific/Auckland",
"skraak isnight --file recording.wav --lat 51.51 --lng -0.13 | jq '.solar_night'",
)
if err := fs.Parse(args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if err := requireFlags(fs, map[string]any{"--file": *filePath}); err != nil {
return err
}
if *lat == 0 && *lng == 0 {
fs.Usage()
return fmt.Errorf("--lat and --lng are required")
}
output, err := calls.IsNight(calls.IsNightInput{
FilePath: *filePath,
Lat: *lat,
Lng: *lng,
Timezone: *timezone,
})
if err != nil {
return fmt.Errorf("computing night status: %w", err)
}
if *brief {
enc := json.NewEncoder(os.Stdout)
if err := enc.Encode(map[string]any{
"file_path": output.FilePath,
"solar_night": output.SolarNight,
}); err != nil {
return fmt.Errorf("encoding output: %w", err)
}
} else {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(output); err != nil {
return fmt.Errorf("encoding output: %w", err)
}
}
return nil
}