fix: batch upload to HRDLog instead of one by one
This commit is contained in:
@@ -5223,12 +5223,15 @@ func (a *App) runManualUpload(svc extsvc.Service, ids []int64, cfg extsvc.Extern
|
||||
}
|
||||
emit(fmt.Sprintf("LoTW: %d QSO(s) uploaded", uploaded))
|
||||
}
|
||||
} else if svc == extsvc.ServiceClublog {
|
||||
// Club Log accepts a whole ADIF file (putlogs.php) and dedupes
|
||||
// server-side, so upload in chunks instead of one realtime.php request
|
||||
// per QSO. Chunked so a single failure doesn't lose the whole run and
|
||||
// the user sees progress.
|
||||
const clublogChunk = 100
|
||||
} else if svc == extsvc.ServiceClublog || svc == extsvc.ServiceHRDLog {
|
||||
// Club Log and HRDLog both accept a whole ADIF document in one request
|
||||
// and dedupe server-side, so upload in chunks instead of one request per
|
||||
// QSO. Chunked so a single failure doesn't lose the whole run and the
|
||||
// user sees progress.
|
||||
name, chunk := "Club Log", 100
|
||||
if svc == extsvc.ServiceHRDLog {
|
||||
name, chunk = "HRDLog", 50
|
||||
}
|
||||
type item struct {
|
||||
id int64
|
||||
rec string
|
||||
@@ -5248,9 +5251,9 @@ func (a *App) runManualUpload(svc extsvc.Service, ids []int64, cfg extsvc.Extern
|
||||
}
|
||||
items = append(items, item{id: id, rec: rec, call: call})
|
||||
}
|
||||
emit(fmt.Sprintf("Club Log: uploading %d QSO(s) in batches of %d…", len(items), clublogChunk))
|
||||
for start := 0; start < len(items); start += clublogChunk {
|
||||
end := start + clublogChunk
|
||||
emit(fmt.Sprintf("%s: uploading %d QSO(s) in batches of %d…", name, len(items), chunk))
|
||||
for start := 0; start < len(items); start += chunk {
|
||||
end := start + chunk
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
@@ -5260,19 +5263,25 @@ func (a *App) runManualUpload(svc extsvc.Service, ids []int64, cfg extsvc.Extern
|
||||
recs[i] = it.rec
|
||||
}
|
||||
doc := adif.BatchRecordsADIF(recs)
|
||||
res, err := extsvc.UploadClublogADIF(ctx, nil, cfg.Clublog, doc)
|
||||
var res extsvc.UploadResult
|
||||
var err error
|
||||
if svc == extsvc.ServiceHRDLog {
|
||||
res, err = extsvc.UploadHRDLogADIF(ctx, nil, cfg.HRDLog.Callsign, cfg.HRDLog.Code, doc)
|
||||
} else {
|
||||
res, err = extsvc.UploadClublogADIF(ctx, nil, cfg.Clublog, doc)
|
||||
}
|
||||
if err == nil && res.OK {
|
||||
for _, it := range batch {
|
||||
a.markExtUploaded(svc, it.id, "")
|
||||
uploaded++
|
||||
}
|
||||
emit(fmt.Sprintf("Club Log: %d/%d uploaded", end, len(items)))
|
||||
emit(fmt.Sprintf("%s: %d/%d uploaded", name, end, len(items)))
|
||||
} else {
|
||||
msg := res.Message
|
||||
if err != nil {
|
||||
msg = err.Error()
|
||||
}
|
||||
emit(fmt.Sprintf("Club Log: batch of %d FAILED: %s", len(batch), msg))
|
||||
emit(fmt.Sprintf("%s: batch of %d FAILED: %s", name, len(batch), msg))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -112,7 +112,8 @@ export function BulkEditModal({ open, ids, onClose, onApplied }: Props) {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-[90px_1fr] gap-3 items-center py-2">
|
||||
<div className="px-5 py-2 space-y-3">
|
||||
<div className="grid grid-cols-[90px_1fr] gap-3 items-center">
|
||||
<Label className="text-sm">Field</Label>
|
||||
<Select value={field} onValueChange={setField}>
|
||||
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
|
||||
@@ -151,6 +152,7 @@ export function BulkEditModal({ open, ids, onClose, onApplied }: Props) {
|
||||
<span className="font-mono">{effectiveValue === '' ? '(blank)' : effectiveValue}</span> on {ids.length} QSO{ids.length > 1 ? 's' : ''}.
|
||||
</div>
|
||||
{error && <div className="text-xs text-rose-700">{error}</div>}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={busy}>Cancel</Button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -116,6 +117,62 @@ func UploadHRDLog(ctx context.Context, client *http.Client, callsign, code, adif
|
||||
return UploadResult{OK: false, Message: reason}, fmt.Errorf("hrdlog: upload failed: %s", reason)
|
||||
}
|
||||
|
||||
// UploadHRDLogADIF pushes a WHOLE ADIF document (header + many records) to
|
||||
// HRDLog.net in one NewEntry request — HRDLog parses every <eor> record and
|
||||
// replies "<insert>N" with the number actually inserted (duplicates aren't
|
||||
// counted but aren't errors). Use this for bulk uploads instead of calling
|
||||
// UploadHRDLog once per QSO.
|
||||
func UploadHRDLogADIF(ctx context.Context, client *http.Client, callsign, code, adifDoc string) (UploadResult, error) {
|
||||
callsign = strings.ToUpper(strings.TrimSpace(callsign))
|
||||
code = strings.TrimSpace(code)
|
||||
if callsign == "" {
|
||||
return UploadResult{}, fmt.Errorf("hrdlog: station callsign not set")
|
||||
}
|
||||
if code == "" {
|
||||
return UploadResult{}, fmt.Errorf("hrdlog: upload code not set")
|
||||
}
|
||||
if strings.TrimSpace(adifDoc) == "" {
|
||||
return UploadResult{}, fmt.Errorf("hrdlog: empty adif")
|
||||
}
|
||||
|
||||
body, err := hrdlogPost(ctx, client, callsign, code, adifDoc)
|
||||
if err != nil {
|
||||
return UploadResult{OK: false, Message: body}, err
|
||||
}
|
||||
if reason := authErrHRDLog(body); reason != "" {
|
||||
return UploadResult{OK: false, Message: reason}, fmt.Errorf("hrdlog: %s", reason)
|
||||
}
|
||||
if n, ok := parseHRDLogInsert(body); ok {
|
||||
return UploadResult{OK: true, Message: fmt.Sprintf("%d added", n)}, nil
|
||||
}
|
||||
if strings.Contains(strings.ToLower(body), "<error>") {
|
||||
return UploadResult{OK: false, Message: body}, fmt.Errorf("hrdlog: %s", body)
|
||||
}
|
||||
return UploadResult{OK: true, Message: "uploaded"}, nil
|
||||
}
|
||||
|
||||
// parseHRDLogInsert reads N from "<insert>N" (or "<insert>N</insert>").
|
||||
func parseHRDLogInsert(body string) (int, bool) {
|
||||
b := strings.ToLower(body)
|
||||
i := strings.Index(b, "<insert>")
|
||||
if i < 0 {
|
||||
return 0, false
|
||||
}
|
||||
rest := b[i+len("<insert>"):]
|
||||
j := 0
|
||||
for j < len(rest) && rest[j] >= '0' && rest[j] <= '9' {
|
||||
j++
|
||||
}
|
||||
if j == 0 {
|
||||
return 0, false
|
||||
}
|
||||
n, err := strconv.Atoi(rest[:j])
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
// TestHRDLog validates the configured HRDLog credentials with a REAL request:
|
||||
// it posts an empty ADIF so nothing is inserted, then checks for HRDLog's auth
|
||||
// errors. A wrong upload code comes back as "Invalid token", a wrong callsign
|
||||
|
||||
Reference in New Issue
Block a user