Bladeren bron

Initial commit

Colin Powell 2 weken geleden
commit
e77a4d4237
5 gewijzigde bestanden met toevoegingen van 175 en 0 verwijderingen
  1. 2 0
      .gitignore
  2. BIN
      build/camcap
  3. BIN
      build/camcap_linux
  4. 165 0
      camcap.go
  5. 8 0
      camcap.json.example

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+images
+camcap.json

BIN
build/camcap


BIN
build/camcap_linux


+ 165 - 0
camcap.go

@@ -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)
+}
+

+ 8 - 0
camcap.json.example

@@ -0,0 +1,8 @@
+{
+  "images_path": "images",
+  "ntfy_url": "https://ntfy.sh/"
+  "poll_interval_minutes": 5,
+  "cameras": {
+    "goats": { "host": "goat-cam.service", "port": "8082" },
+  }
+}