first commit
This commit is contained in:
260
internal/watchlist/watchlist.go
Normal file
260
internal/watchlist/watchlist.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package watchlist
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
Callsign string `json:"callsign"`
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
LastSeenStr string `json:"lastSeenStr"`
|
||||
AddedAt time.Time `json:"addedAt"`
|
||||
SpotCount int `json:"spotCount"`
|
||||
IsContest bool `json:"isContest"`
|
||||
Notify bool `json:"notify"`
|
||||
|
||||
// ActiveSpotIDs n'est pas sérialisé — reconstruit en mémoire
|
||||
activeSpotIDs map[int64]bool
|
||||
}
|
||||
|
||||
type Watchlist struct {
|
||||
entries map[string]*Entry
|
||||
filePath string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func New(filePath string) *Watchlist {
|
||||
w := &Watchlist{
|
||||
entries: make(map[string]*Entry),
|
||||
filePath: filePath,
|
||||
}
|
||||
w.load()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *Watchlist) load() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(w.filePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var entries []Entry
|
||||
if err := json.Unmarshal(data, &entries); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range entries {
|
||||
e := &entries[i]
|
||||
e.activeSpotIDs = make(map[int64]bool)
|
||||
if e.LastSeen.IsZero() {
|
||||
e.LastSeenStr = "Never"
|
||||
} else {
|
||||
e.LastSeenStr = FormatLastSeen(e.LastSeen)
|
||||
}
|
||||
w.entries[e.Callsign] = e
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watchlist) save() error {
|
||||
entries := make([]Entry, 0, len(w.entries))
|
||||
for _, e := range w.entries {
|
||||
entries = append(entries, *e)
|
||||
}
|
||||
data, err := json.MarshalIndent(entries, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(w.filePath, data, 0644)
|
||||
}
|
||||
|
||||
func (w *Watchlist) Add(callsign string) error {
|
||||
return w.add(callsign, false)
|
||||
}
|
||||
|
||||
func (w *Watchlist) AddContest(callsign string) error {
|
||||
return w.add(callsign, true)
|
||||
}
|
||||
|
||||
func (w *Watchlist) add(callsign string, isContest bool) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
callsign = strings.ToUpper(strings.TrimSpace(callsign))
|
||||
if callsign == "" {
|
||||
return fmt.Errorf("callsign cannot be empty")
|
||||
}
|
||||
if _, exists := w.entries[callsign]; exists {
|
||||
return fmt.Errorf("callsign already in watchlist")
|
||||
}
|
||||
|
||||
w.entries[callsign] = &Entry{
|
||||
Callsign: callsign,
|
||||
AddedAt: time.Now(),
|
||||
LastSeenStr: "Never",
|
||||
activeSpotIDs: make(map[int64]bool),
|
||||
IsContest: isContest,
|
||||
}
|
||||
|
||||
return w.save()
|
||||
}
|
||||
|
||||
func (w *Watchlist) Remove(callsign string) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
callsign = strings.ToUpper(strings.TrimSpace(callsign))
|
||||
if _, exists := w.entries[callsign]; !exists {
|
||||
return fmt.Errorf("callsign not in watchlist")
|
||||
}
|
||||
delete(w.entries, callsign)
|
||||
return w.save()
|
||||
}
|
||||
|
||||
func (w *Watchlist) SetNotify(callsign string, notify bool) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
callsign = strings.ToUpper(strings.TrimSpace(callsign))
|
||||
e, exists := w.entries[callsign]
|
||||
if !exists {
|
||||
return fmt.Errorf("callsign not found")
|
||||
}
|
||||
e.Notify = notify
|
||||
return w.save()
|
||||
}
|
||||
|
||||
func (w *Watchlist) Matches(callsign string) bool {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
_, ok := w.entries[strings.ToUpper(callsign)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (w *Watchlist) GetEntry(callsign string) *Entry {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
e, ok := w.entries[strings.ToUpper(callsign)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
copy := *e
|
||||
return ©
|
||||
}
|
||||
|
||||
func (w *Watchlist) GetAll() []Entry {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
entries := make([]Entry, 0, len(w.entries))
|
||||
for _, e := range w.entries {
|
||||
cp := *e
|
||||
if !cp.LastSeen.IsZero() {
|
||||
cp.LastSeenStr = FormatLastSeen(cp.LastSeen)
|
||||
}
|
||||
entries = append(entries, cp)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (w *Watchlist) GetAllCallsigns() []string {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
callsigns := make([]string, 0, len(w.entries))
|
||||
for cs := range w.entries {
|
||||
callsigns = append(callsigns, cs)
|
||||
}
|
||||
return callsigns
|
||||
}
|
||||
|
||||
func (w *Watchlist) MarkSeen(callsign string) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
e, ok := w.entries[strings.ToUpper(callsign)]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
e.LastSeen = time.Now()
|
||||
e.LastSeenStr = FormatLastSeen(e.LastSeen)
|
||||
e.SpotCount++
|
||||
}
|
||||
|
||||
func (w *Watchlist) AddActiveSpot(callsign string, spotID int64) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
e, ok := w.entries[strings.ToUpper(callsign)]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if e.activeSpotIDs == nil {
|
||||
e.activeSpotIDs = make(map[int64]bool)
|
||||
}
|
||||
e.activeSpotIDs[spotID] = true
|
||||
}
|
||||
|
||||
func (w *Watchlist) RemoveActiveSpot(spotID int64) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
for _, e := range w.entries {
|
||||
delete(e.activeSpotIDs, spotID)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watchlist) GetAllWithActiveStatus() []map[string]interface{} {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
result := make([]map[string]interface{}, 0, len(w.entries))
|
||||
for _, e := range w.entries {
|
||||
lastSeenStr := "Never"
|
||||
if !e.LastSeen.IsZero() {
|
||||
lastSeenStr = FormatLastSeen(e.LastSeen)
|
||||
}
|
||||
result = append(result, map[string]interface{}{
|
||||
"callsign": e.Callsign,
|
||||
"lastSeen": e.LastSeen,
|
||||
"lastSeenStr": lastSeenStr,
|
||||
"addedAt": e.AddedAt,
|
||||
"spotCount": e.SpotCount,
|
||||
"notify": e.Notify,
|
||||
"isContest": e.IsContest,
|
||||
"hasActiveSpots": len(e.activeSpotIDs) > 0,
|
||||
"activeCount": len(e.activeSpotIDs),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func FormatLastSeen(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "Never"
|
||||
}
|
||||
d := time.Since(t)
|
||||
switch {
|
||||
case d < time.Minute:
|
||||
return "Just now"
|
||||
case d < time.Hour:
|
||||
m := int(d.Minutes())
|
||||
if m == 1 {
|
||||
return "1 minute ago"
|
||||
}
|
||||
return fmt.Sprintf("%d minutes ago", m)
|
||||
case d < 24*time.Hour:
|
||||
h := int(d.Hours())
|
||||
if h == 1 {
|
||||
return "1 hour ago"
|
||||
}
|
||||
return fmt.Sprintf("%d hours ago", h)
|
||||
default:
|
||||
days := int(d.Hours() / 24)
|
||||
if days == 1 {
|
||||
return "1 day ago"
|
||||
}
|
||||
return fmt.Sprintf("%d days ago", days)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user