package main import ( "encoding/json" "fmt" "os" "strings" "sync" "time" ) type WatchlistEntry struct { Callsign string `json:"callsign"` LastSeen time.Time `json:"lastSeen"` LastSeenStr string `json:"lastSeenStr"` AddedAt time.Time `json:"addedAt"` SpotCount int `json:"spotCount"` PlaySound bool `json:"playSound"` } type Watchlist struct { entries map[string]*WatchlistEntry filePath string mutex sync.RWMutex } func NewWatchlist(filePath string) *Watchlist { w := &Watchlist{ entries: make(map[string]*WatchlistEntry), filePath: filePath, } w.load() return w } func (w *Watchlist) load() { w.mutex.Lock() defer w.mutex.Unlock() data, err := os.ReadFile(w.filePath) if err != nil { if !os.IsNotExist(err) { Log.Errorf("Error reading watchlist file: %v", err) } else { Log.Debug("Watchlist file does not exist yet, will be created on first save") } return } var entries []WatchlistEntry if err := json.Unmarshal(data, &entries); err != nil { Log.Errorf("Error parsing watchlist file: %v", err) return } for i := range entries { entry := &entries[i] if !entry.LastSeen.IsZero() { entry.LastSeenStr = formatLastSeen(entry.LastSeen) } else { entry.LastSeenStr = "Never" } w.entries[entry.Callsign] = entry } Log.Infof("Loaded %d entries from watchlist", len(w.entries)) } func (w *Watchlist) saveUnsafe() error { entries := make([]WatchlistEntry, 0, len(w.entries)) for _, entry := range w.entries { entries = append(entries, *entry) } data, err := json.MarshalIndent(entries, "", " ") if err != nil { Log.Errorf("Error marshaling watchlist: %v", err) return fmt.Errorf("error marshaling watchlist: %w", err) } if err := os.WriteFile(w.filePath, data, 0644); err != nil { Log.Errorf("Error writing watchlist file %s: %v", w.filePath, err) return fmt.Errorf("error writing watchlist file: %w", err) } Log.Debugf("Watchlist saved successfully (%d entries)", len(entries)) return nil } func (w *Watchlist) save() error { w.mutex.RLock() defer w.mutex.RUnlock() return w.saveUnsafe() } func (w *Watchlist) Add(callsign string) error { w.mutex.Lock() defer w.mutex.Unlock() callsign = strings.ToUpper(strings.TrimSpace(callsign)) Log.Debugf("Attempting to add callsign: %s", callsign) if callsign == "" { Log.Warn("Attempted to add empty callsign") return fmt.Errorf("callsign cannot be empty") } if _, exists := w.entries[callsign]; exists { Log.Warnf("Callsign %s already exists in watchlist", callsign) return fmt.Errorf("callsign already in watchlist") } w.entries[callsign] = &WatchlistEntry{ Callsign: callsign, AddedAt: time.Now(), LastSeen: time.Time{}, LastSeenStr: "Never", SpotCount: 0, PlaySound: true, } Log.Infof("Added %s to watchlist", callsign) if err := w.saveUnsafe(); err != nil { Log.Errorf("Failed to save watchlist after adding %s: %v", callsign, err) return err } Log.Debugf("Watchlist saved successfully after adding %s", callsign) return nil } func (w *Watchlist) Remove(callsign string) error { w.mutex.Lock() defer w.mutex.Unlock() callsign = strings.ToUpper(strings.TrimSpace(callsign)) if _, exists := w.entries[callsign]; !exists { Log.Warnf("Attempted to remove non-existent callsign: %s", callsign) return fmt.Errorf("callsign not in watchlist") } delete(w.entries, callsign) Log.Infof("Removed %s from watchlist", callsign) if err := w.saveUnsafe(); err != nil { Log.Errorf("Failed to save watchlist after removing %s: %v", callsign, err) return err } return nil } func (w *Watchlist) UpdateSound(callsign string, playSound bool) error { w.mutex.Lock() defer w.mutex.Unlock() callsign = strings.ToUpper(strings.TrimSpace(callsign)) entry, exists := w.entries[callsign] if !exists { Log.Warnf("Attempted to update sound for non-existent callsign: %s", callsign) return fmt.Errorf("callsign not in watchlist") } entry.PlaySound = playSound Log.Debugf("Updated sound setting for %s to %v", callsign, playSound) if err := w.saveUnsafe(); err != nil { Log.Errorf("Failed to save watchlist after updating sound for %s: %v", callsign, err) return err } return nil } func (w *Watchlist) MarkSeen(callsign string) { w.mutex.Lock() defer w.mutex.Unlock() callsign = strings.ToUpper(strings.TrimSpace(callsign)) entry, exists := w.entries[callsign] if !exists { return } entry.LastSeen = time.Now() entry.LastSeenStr = formatLastSeen(entry.LastSeen) entry.SpotCount++ Log.Debugf("Marked %s as seen (count: %d)", callsign, entry.SpotCount) } func (w *Watchlist) Matches(callsign string) bool { w.mutex.RLock() defer w.mutex.RUnlock() callsign = strings.ToUpper(callsign) for pattern := range w.entries { if callsign == pattern || strings.HasPrefix(callsign, pattern) { return true } } return false } func (w *Watchlist) GetEntry(callsign string) *WatchlistEntry { w.mutex.RLock() defer w.mutex.RUnlock() callsign = strings.ToUpper(callsign) for pattern, entry := range w.entries { if callsign == pattern || strings.HasPrefix(callsign, pattern) { entryCopy := *entry return &entryCopy } } return nil } func (w *Watchlist) GetAll() []WatchlistEntry { w.mutex.RLock() defer w.mutex.RUnlock() entries := make([]WatchlistEntry, 0, len(w.entries)) for _, entry := range w.entries { entryCopy := *entry if !entryCopy.LastSeen.IsZero() { entryCopy.LastSeenStr = formatLastSeen(entryCopy.LastSeen) } entries = append(entries, entryCopy) } return entries } func (w *Watchlist) GetAllCallsigns() []string { w.mutex.RLock() defer w.mutex.RUnlock() callsigns := make([]string, 0, len(w.entries)) for callsign := range w.entries { callsigns = append(callsigns, callsign) } return callsigns } func formatLastSeen(t time.Time) string { if t.IsZero() { return "Never" } duration := time.Since(t) if duration < time.Minute { return "Just now" } else if duration < time.Hour { mins := int(duration.Minutes()) if mins == 1 { return "1 minute ago" } return fmt.Sprintf("%d minutes ago", mins) } else if duration < 24*time.Hour { hours := int(duration.Hours()) if hours == 1 { return "1 hour ago" } return fmt.Sprintf("%d hours ago", hours) } else { days := int(duration.Hours() / 24) if days == 1 { return "1 day ago" } return fmt.Sprintf("%d days ago", days) } }