Initial commit

This commit is contained in:
2025-09-29 20:48:48 -04:00
commit e77a4d4237
5 changed files with 175 additions and 0 deletions

165
camcap.go Normal file
View File

@ -0,0 +1,165 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sync"
"time"
"strings"
)
// Camera represents a single camera
type Camera struct {
Host string `json:"host"`
Port string `json:"port"`
}
// Config represents the full JSON config
type Config struct {
ImagesPath string `json:"images_path"`
NtfyURL string `json:"ntfy_url"`
PollIntervalMinutes int `json:"poll_interval_minutes"`
Cameras map[string]Camera `json:"cameras"`
}
var config Config
func main() {
loadConfig()
interval := time.Duration(config.PollIntervalMinutes) * time.Minute
ticker := time.NewTicker(interval)
defer ticker.Stop()
// Run immediately at startup
go pollImages()
for range ticker.C {
go pollImages()
}
select {} // keep program running
}
// loadConfig searches multiple locations for the JSON config
func loadConfig() {
paths := []string{
"/usr/local/etc/camcap.json",
"/usr/etc/camcap.json",
"./camcap.json",
}
var configFile string
for _, path := range paths {
if _, err := os.Stat(path); err == nil {
configFile = path
break
}
}
if configFile == "" {
panic("No configuration file found in /usr/local/etc, /usr/etc, or current directory")
}
f, err := os.Open(configFile)
if err != nil {
panic(err)
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&config); err != nil {
panic(err)
}
fmt.Println("Loaded config from:", configFile)
}
// pollImages downloads snapshots concurrently
func pollImages() {
fmt.Println("Starting image poll at", time.Now())
var wg sync.WaitGroup
errCh := make(chan error, len(config.Cameras))
for title, cam := range config.Cameras {
url := fmt.Sprintf("http://%s.service:%s/snapshot", cam.Host, cam.Port)
wg.Add(1)
go func(title, url string) {
defer wg.Done()
if err := downloadImageWithTitle(url, title); err != nil {
errCh <- fmt.Errorf("%s: %v", title, err)
} else {
fmt.Println("Downloaded:", title)
}
}(title, url)
}
wg.Wait()
close(errCh)
for err := range errCh {
fmt.Println("Error:", err)
}
fmt.Println("Finished image poll at", time.Now())
}
// downloadImageWithTitle saves the image to timestamped directories
func downloadImageWithTitle(url, title string) error {
today := time.Now()
dir := filepath.Join(config.ImagesPath, title,
fmt.Sprintf("%d", today.Year()),
fmt.Sprintf("%02d", today.Month()),
fmt.Sprintf("%02d", today.Day()))
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
timestamp := today.Format("20060102150405")
fileName := fmt.Sprintf("%s-%s.jpg", title, timestamp)
filePath := filepath.Join(dir, fileName)
resp, err := http.Get(url)
if err != nil {
notifyError(fmt.Sprintf("Connection error for %s: %v", title, err))
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
notifyError(fmt.Sprintf("HTTP error for %s: %s", title, resp.Status))
return fmt.Errorf("bad status: %s", resp.Status)
}
out, err := os.Create(filePath)
if err != nil {
notifyError(fmt.Sprintf("File creation error for %s: %v", title, err))
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
notifyError(fmt.Sprintf("File write error for %s: %v", title, err))
}
return err
}
// notifyError posts errors to ntfy
func notifyError(message string) {
resp, err := http.Post(config.NtfyURL, "text/plain", strings.NewReader(message))
if err != nil {
fmt.Println("Failed to send ntfy notification:", err)
return
}
defer resp.Body.Close()
fmt.Println("Sent ntfy error:", message)
}