first commit

This commit is contained in:
2026-03-17 20:20:23 +01:00
commit 354c7a9d99
32 changed files with 3253 additions and 0 deletions

89
internal/spot/parser.go Normal file
View File

@@ -0,0 +1,89 @@
package spot
import (
"regexp"
"strconv"
"strings"
)
// Regex pour les deux formats de spots cluster
var (
// Format standard : DX de SPOTTER: FREQ DX MODE COMMENT TIME
SpotRe = regexp.MustCompile(`(?i)DX\sde\s([\w\d\/]+?)(?:-[#\d-]+)?\s*:\s*(\d+\.\d+)\s+([\w\d\/]+)\s+(?:(CW|SSB|FT8|FT4|RTTY|USB|LSB|FM)\s+)?(.+?)\s+(\d{4}Z)`)
// Format court : FREQ DX DATE TIME COMMENT <SPOTTER>
SpotReShort = regexp.MustCompile(`^(\d+\.\d+)\s+([\w\d\/]+)\s+\d{2}-\w{3}-\d{4}\s+(\d{4}Z)\s+(.+?)\s*<([\w\d\/]+)>\s*$`)
// Détection rapide du format court
ShortSpotDetectRe = regexp.MustCompile(`^\d+\.\d+\s+[\w\d\/]+\s+\d{2}-\w{3}-\d{4}`)
)
// ParseResult contient le spot parsé et une éventuelle erreur
type ParseResult struct {
Spot *Spot
Err error
Skipped bool // true si la ligne n'est pas un spot (pas une erreur)
}
// ParseLine tente de parser une ligne brute du cluster en Spot
// Retourne nil si la ligne n'est pas un spot DX
func ParseLine(line string, clusterName string) *Spot {
// Détecter si c'est un spot
isSpot := strings.Contains(line, "DX de ") || ShortSpotDetectRe.MatchString(line)
if !isSpot {
return nil
}
match := SpotRe.FindStringSubmatch(line)
if len(match) > 0 {
return parseStandardFormat(match, clusterName)
}
match = SpotReShort.FindStringSubmatch(line)
if len(match) > 0 {
return parseShortFormat(match, clusterName)
}
return nil
}
func parseStandardFormat(match []string, clusterName string) *Spot {
freqKHz := parseFreq(match[2])
return &Spot{
Spotter: match[1],
FrequencyKHz: freqKHz,
DX: match[3],
Mode: match[4],
Comment: strings.TrimSpace(match[5]),
Time: match[6],
ClusterName: clusterName,
Source: SourceCluster,
}
}
func parseShortFormat(match []string, clusterName string) *Spot {
freqKHz := parseFreq(match[1])
return &Spot{
FrequencyKHz: freqKHz,
DX: match[2],
Time: match[3],
Comment: strings.TrimSpace(match[4]),
Spotter: match[5],
ClusterName: clusterName,
Source: SourceCluster,
}
}
// parseFreq parse une fréquence string en kHz float64
// Gère kHz (>1000) et MHz (<1000) automatiquement
func parseFreq(s string) float64 {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
// Si < 1000 c'est en MHz, on convertit en kHz
if f < 1000 {
return f * 1000.0
}
return f
}

97
internal/spot/spot.go Normal file
View File

@@ -0,0 +1,97 @@
package spot
import (
"fmt"
"time"
)
// SpotSource identifie l'origine d'un spot
type SpotSource int
const (
SourceCluster SpotSource = iota // Spot reçu depuis un cluster DX telnet
SourceManual // Spot ajouté manuellement depuis l'UI
)
// Spot est la struct universelle — remplace TelnetSpot + FlexSpot
// Plus de conversion entre les deux, tout passe par cette struct unique
type Spot struct {
// --- Persistance ---
ID int64
// --- Identité ---
DX string
Spotter string
// --- Fréquence ---
// FrequencyKHz est la source de vérité interne
// FrequencyMHz est le format string attendu par FlexRadio (ex: "14.195000")
FrequencyKHz float64
FrequencyMHz string
Band string
// --- Mode ---
Mode string
// --- Métadonnées cluster ---
Comment string
Time string // Format "1234Z"
Timestamp int64 // Unix timestamp
ReceivedAt time.Time
ClusterName string
Source SpotSource
// --- DXCC ---
DXCC string
CountryName string
// --- Flags Log4OM (calculés depuis la DB Log4OM) ---
NewDXCC bool
NewBand bool
NewMode bool
NewSlot bool
CallsignWorked bool
// --- Watchlist ---
InWatchlist bool
WatchlistNotify bool
// --- POTA / SOTA ---
POTARef string
SOTARef string
ParkName string
SummitName string
// --- FlexRadio panadapter ---
FlexSpotNumber int
CommandNumber int
Color string
BackgroundColor string
Priority string
LifeTime string
OriginalComment string
}
// FreqMHzString retourne la fréquence formatée pour FlexRadio
// ex: 14195.0 kHz → "14.195000"
func (s *Spot) FreqMHzString() string {
if s.FrequencyMHz != "" {
return s.FrequencyMHz
}
if s.FrequencyKHz > 1000 {
return formatMHz(s.FrequencyKHz / 1000.0)
}
return formatMHz(s.FrequencyKHz)
}
// FreqKHz retourne la fréquence en kHz depuis le champ disponible
func (s *Spot) FreqKHz() float64 {
if s.FrequencyKHz > 0 {
return s.FrequencyKHz
}
return 0
}
func formatMHz(mhz float64) string {
return fmt.Sprintf("%.6f", mhz)
}