285 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
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)
 | 
						|
	}
 | 
						|
}
 |