Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

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
}