award
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user