chore: release v0.21.3
This commit is contained in:
+102
-29
@@ -109,22 +109,30 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, p := range providers {
|
||||
r, err := p.Lookup(ctx, call)
|
||||
if err == nil {
|
||||
r.Callsign = call
|
||||
r.Source = p.Name()
|
||||
r.FetchedAt = time.Now().UTC()
|
||||
fillFromDXCC(&r, dxcc)
|
||||
normalizeNames(&r)
|
||||
_ = m.cache.Put(ctx, r)
|
||||
return r, nil
|
||||
// An operational suffix (/M, /P, …) is never registered as such: skip the
|
||||
// futile query on the slashed form and let the home-call pass below do the one
|
||||
// request that can actually answer.
|
||||
_, opOnly := stripOpSuffix(call)
|
||||
if opOnly {
|
||||
LogSink("lookup: %s carries only an operational suffix — querying the bare call", call)
|
||||
} else {
|
||||
for _, p := range providers {
|
||||
r, err := p.Lookup(ctx, call)
|
||||
if err == nil {
|
||||
r.Callsign = call
|
||||
r.Source = p.Name()
|
||||
r.FetchedAt = time.Now().UTC()
|
||||
fillFromDXCC(&r, dxcc)
|
||||
normalizeNames(&r)
|
||||
_ = m.cache.Put(ctx, r)
|
||||
return r, nil
|
||||
}
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
lastErr = fmt.Errorf("%s: %w", p.Name(), err)
|
||||
}
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
lastErr = fmt.Errorf("%s: %w", p.Name(), err)
|
||||
}
|
||||
|
||||
// Portable / slashed call not found under its full form: the operator's
|
||||
@@ -135,6 +143,10 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
|
||||
for _, p := range providers {
|
||||
r, err := p.Lookup(ctx, home)
|
||||
if err != nil {
|
||||
// Logged, because this is where a portable lookup silently dies: the
|
||||
// error is swallowed to try the next provider, and the operator only
|
||||
// ever sees the cty.dat fallback with no clue why.
|
||||
LogSink("lookup: %s → home call %s failed on %s: %v", call, home, p.Name(), err)
|
||||
continue
|
||||
}
|
||||
r.Callsign = call
|
||||
@@ -177,6 +189,46 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
|
||||
return Result{}, lastErr
|
||||
}
|
||||
|
||||
// LogSink receives this package's diagnostic lines (which call was actually
|
||||
// queried, and why a lookup fell back). Set to applog.Printf by the app.
|
||||
var LogSink = func(string, ...any) {}
|
||||
|
||||
// opSuffixes are OPERATIONAL suffixes: they describe how the operator is working
|
||||
// — mobile, maritime, aeronautical, portable, low power — not who they are or
|
||||
// where. No provider has a record filed under "F4LYI/M", so querying that form
|
||||
// is a round trip that cannot succeed.
|
||||
//
|
||||
// It was worse than merely wasted: it spent the lookup's time budget, so the
|
||||
// home-call retry that followed ran out of time and the entry fell back to
|
||||
// cty.dat for every /M and /P call, even though the operator was on QRZ. These
|
||||
// go straight to the bare callsign instead.
|
||||
//
|
||||
// Everything else after a slash is NOT this: JW/, VP8/ change the DXCC entity,
|
||||
// and /8 or /W6 change the call area. Those forms can be registered in their own
|
||||
// right and must be looked up exactly as entered.
|
||||
var opSuffixes = map[string]bool{"M": true, "MM": true, "AM": true, "P": true, "QRP": true}
|
||||
|
||||
// stripOpSuffix returns the bare callsign when call carries nothing but
|
||||
// operational suffixes ("F4LYI/M" → "F4LYI", true). Reports false for anything
|
||||
// that changes entity or area ("JW/OR1A", "F4BPO/8"), and for a call whose base
|
||||
// part isn't callsign-shaped.
|
||||
func stripOpSuffix(call string) (string, bool) {
|
||||
if !strings.ContainsRune(call, '/') {
|
||||
return call, false
|
||||
}
|
||||
parts := strings.Split(call, "/")
|
||||
base := strings.TrimSpace(parts[0])
|
||||
if len(base) < 3 || !strings.ContainsAny(base, "0123456789") {
|
||||
return call, false // "JW/OR1A": the first part is a prefix, not the callsign
|
||||
}
|
||||
for _, p := range parts[1:] {
|
||||
if !opSuffixes[strings.ToUpper(strings.TrimSpace(p))] {
|
||||
return call, false
|
||||
}
|
||||
}
|
||||
return base, true
|
||||
}
|
||||
|
||||
// homeCall extracts the operator's home callsign from a slashed/portable call
|
||||
// so its provider record (name/QTH/QSL) can be fetched when the full form isn't
|
||||
// registered: JW/OR1A → OR1A, DL/F4NIE → F4NIE, F4BPO/P → F4BPO, VP8/F4BPO →
|
||||
@@ -266,12 +318,30 @@ func fillFromDXCC(r *Result, dxcc DXCCResolver) bool {
|
||||
return false
|
||||
}
|
||||
filled := false
|
||||
if country != "" { r.Country = country; filled = true }
|
||||
if cont != "" { r.Continent = cont; filled = true }
|
||||
if cqz != 0 { r.CQZ = cqz; filled = true }
|
||||
if ituz != 0 { r.ITUZ = ituz; filled = true }
|
||||
if lat != 0 && r.Lat == 0 { r.Lat = lat; filled = true }
|
||||
if lon != 0 && r.Lon == 0 { r.Lon = lon; filled = true }
|
||||
if country != "" {
|
||||
r.Country = country
|
||||
filled = true
|
||||
}
|
||||
if cont != "" {
|
||||
r.Continent = cont
|
||||
filled = true
|
||||
}
|
||||
if cqz != 0 {
|
||||
r.CQZ = cqz
|
||||
filled = true
|
||||
}
|
||||
if ituz != 0 {
|
||||
r.ITUZ = ituz
|
||||
filled = true
|
||||
}
|
||||
if lat != 0 && r.Lat == 0 {
|
||||
r.Lat = lat
|
||||
filled = true
|
||||
}
|
||||
if lon != 0 && r.Lon == 0 {
|
||||
r.Lon = lon
|
||||
filled = true
|
||||
}
|
||||
// cty.dat is authoritative for the *operating* entity: it strips benign
|
||||
// suffixes (/P /M /MM /QRP /A …) and honours real prefixes (DL/F4NIE).
|
||||
// Use its DXCC# when known — this overrides the provider's home-call
|
||||
@@ -279,7 +349,10 @@ func fillFromDXCC(r *Result, dxcc DXCCResolver) bool {
|
||||
// France's 227). Only when cty.dat can't map a slashed call do we drop
|
||||
// the provider's number rather than mislabel.
|
||||
if dxccNum != 0 {
|
||||
if r.DXCC != dxccNum { r.DXCC = dxccNum; filled = true }
|
||||
if r.DXCC != dxccNum {
|
||||
r.DXCC = dxccNum
|
||||
filled = true
|
||||
}
|
||||
} else if strings.ContainsRune(r.Callsign, '/') && r.DXCC != 0 {
|
||||
r.DXCC = 0
|
||||
filled = true
|
||||
@@ -317,13 +390,13 @@ func (c *Cache) Get(ctx context.Context, callsign string) (Result, bool) {
|
||||
source, fetched_at
|
||||
FROM callsign_cache WHERE callsign = ?`, callsign)
|
||||
var (
|
||||
r Result
|
||||
name, qth, addr, state, cnty sql.NullString
|
||||
country, grid, cont, email, qslVia, image sql.NullString
|
||||
src string
|
||||
dxcc, cqz, ituz sql.NullInt64
|
||||
lat, lon sql.NullFloat64
|
||||
fetched string
|
||||
r Result
|
||||
name, qth, addr, state, cnty sql.NullString
|
||||
country, grid, cont, email, qslVia, image sql.NullString
|
||||
src string
|
||||
dxcc, cqz, ituz sql.NullInt64
|
||||
lat, lon sql.NullFloat64
|
||||
fetched string
|
||||
)
|
||||
if err := row.Scan(&r.Callsign, &name, &qth, &addr, &state, &cnty,
|
||||
&country, &grid, &lat, &lon,
|
||||
|
||||
Reference in New Issue
Block a user