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