This commit is contained in:
2026-06-05 22:35:28 +02:00
parent 88623f55df
commit 51d3a734e8
21 changed files with 2613 additions and 153 deletions
+161
View File
@@ -0,0 +1,161 @@
package awardref
import (
"context"
"encoding/csv"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
// Importer downloads and parses a program's reference list into []Ref.
type Importer struct {
AwardCode string
URL string
Fetch func(ctx context.Context, body io.Reader) ([]Ref, error)
}
// Importers is the registry of built-in reference-list updaters, keyed by
// award code. Awards not present here have no online list (manual only).
var Importers = map[string]Importer{
"POTA": {AwardCode: "POTA", URL: "https://pota.app/all_parks.csv", Fetch: parsePOTA},
"SOTA": {AwardCode: "SOTA", URL: "https://www.sotadata.org.uk/summitslist.csv", Fetch: parseSOTA},
"WWFF": {AwardCode: "WWFF", URL: "https://wwff.co/wwff-data/wwff_directory.csv", Fetch: parseWWFF},
}
// CanUpdate reports whether an award has an online reference list.
func CanUpdate(awardCode string) bool {
_, ok := Importers[strings.ToUpper(strings.TrimSpace(awardCode))]
return ok
}
// Download fetches and parses the reference list for an award (does not store).
func Download(ctx context.Context, awardCode string) ([]Ref, error) {
imp, ok := Importers[strings.ToUpper(strings.TrimSpace(awardCode))]
if !ok {
return nil, fmt.Errorf("no online list for award %q", awardCode)
}
req, err := http.NewRequestWithContext(ctx, "GET", imp.URL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "OpsLog")
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("download %s: %w", imp.URL, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("download %s: http %d", imp.URL, resp.StatusCode)
}
return imp.Fetch(ctx, resp.Body)
}
// headerIndex maps lowercased header names to their column index.
func headerIndex(header []string) map[string]int {
m := make(map[string]int, len(header))
for i, h := range header {
m[strings.ToLower(strings.TrimSpace(h))] = i
}
return m
}
func get(rec []string, idx int) string {
if idx < 0 || idx >= len(rec) {
return ""
}
return strings.TrimSpace(rec[idx])
}
// parsePOTA: "reference","name","active","entityId","locationDesc"
func parsePOTA(_ context.Context, body io.Reader) ([]Ref, error) {
r := csv.NewReader(body)
r.FieldsPerRecord = -1
header, err := r.Read()
if err != nil {
return nil, err
}
h := headerIndex(header)
iRef, iName, iActive, iEnt, iLoc := h["reference"], h["name"], h["active"], h["entityid"], h["locationdesc"]
var out []Ref
for {
rec, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
continue
}
if iActive >= 0 && get(rec, iActive) == "0" {
continue
}
dxcc, _ := strconv.Atoi(get(rec, iEnt))
out = append(out, Ref{Code: get(rec, iRef), Name: get(rec, iName), DXCC: dxcc, Group: get(rec, iLoc)})
}
return out, nil
}
// parseSOTA: first line is a title, then header
// SummitCode,AssociationName,RegionName,SummitName,…
func parseSOTA(_ context.Context, body io.Reader) ([]Ref, error) {
r := csv.NewReader(body)
r.FieldsPerRecord = -1
// First record is the "SOTA Summits List (Date=…)" title line — skip it.
if _, err := r.Read(); err != nil {
return nil, err
}
header, err := r.Read()
if err != nil {
return nil, err
}
h := headerIndex(header)
iRef, iName, iAssoc, iRegion := h["summitcode"], h["summitname"], h["associationname"], h["regionname"]
var out []Ref
for {
rec, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
continue
}
out = append(out, Ref{Code: get(rec, iRef), Name: get(rec, iName), Group: get(rec, iAssoc), SubGrp: get(rec, iRegion)})
}
return out, nil
}
// parseWWFF: reference,status,name,…,dxccEnum,… (header-driven)
func parseWWFF(_ context.Context, body io.Reader) ([]Ref, error) {
r := csv.NewReader(body)
r.FieldsPerRecord = -1
header, err := r.Read()
if err != nil {
return nil, err
}
h := headerIndex(header)
iRef, iName, iStatus, iCountry := h["reference"], h["name"], h["status"], h["country"]
iDXCC := h["dxccenum"]
if iDXCC < 0 {
iDXCC = h["dxcc"]
}
var out []Ref
for {
rec, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
continue
}
if iStatus >= 0 && !strings.EqualFold(get(rec, iStatus), "active") {
continue
}
dxcc, _ := strconv.Atoi(get(rec, iDXCC))
out = append(out, Ref{Code: get(rec, iRef), Name: get(rec, iName), DXCC: dxcc, Group: get(rec, iCountry)})
}
return out, nil
}