up
This commit is contained in:
@@ -37,6 +37,7 @@ const (
|
|||||||
// Failsafe is the fallback when Primary returns not-found or errs.
|
// Failsafe is the fallback when Primary returns not-found or errs.
|
||||||
keyLookupPrimary = "lookup.primary"
|
keyLookupPrimary = "lookup.primary"
|
||||||
keyLookupFailsafe = "lookup.failsafe"
|
keyLookupFailsafe = "lookup.failsafe"
|
||||||
|
keyLookupImages = "lookup.download_images" // 1 = expose QRZ ImageURL to UI
|
||||||
|
|
||||||
keyStationCallsign = "station.callsign"
|
keyStationCallsign = "station.callsign"
|
||||||
keyStationOperator = "station.operator"
|
keyStationOperator = "station.operator"
|
||||||
@@ -119,13 +120,14 @@ type StationSettings struct {
|
|||||||
// Primary / Failsafe hold a provider name ("qrz" | "hamqth" | "") to
|
// Primary / Failsafe hold a provider name ("qrz" | "hamqth" | "") to
|
||||||
// route lookups: primary first, failsafe on not-found / error.
|
// route lookups: primary first, failsafe on not-found / error.
|
||||||
type LookupSettings struct {
|
type LookupSettings struct {
|
||||||
QRZUser string `json:"qrz_user"`
|
QRZUser string `json:"qrz_user"`
|
||||||
QRZPassword string `json:"qrz_password"`
|
QRZPassword string `json:"qrz_password"`
|
||||||
HamQTHUser string `json:"hamqth_user"`
|
HamQTHUser string `json:"hamqth_user"`
|
||||||
HamQTHPassword string `json:"hamqth_password"`
|
HamQTHPassword string `json:"hamqth_password"`
|
||||||
Primary string `json:"primary"`
|
Primary string `json:"primary"`
|
||||||
Failsafe string `json:"failsafe"`
|
Failsafe string `json:"failsafe"`
|
||||||
CacheTTLDays int `json:"cache_ttl_days"`
|
DownloadImages bool `json:"download_images"` // show QRZ profile pictures in the UI
|
||||||
|
CacheTTLDays int `json:"cache_ttl_days"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// App is the application context bound to the Wails runtime.
|
// App is the application context bound to the Wails runtime.
|
||||||
@@ -503,9 +505,33 @@ func (a *App) LookupCallsign(callsign string) (lookup.Result, error) {
|
|||||||
if errors.Is(err, lookup.ErrNotFound) {
|
if errors.Is(err, lookup.ErrNotFound) {
|
||||||
return lookup.Result{}, fmt.Errorf("callsign not found")
|
return lookup.Result{}, fmt.Errorf("callsign not found")
|
||||||
}
|
}
|
||||||
|
// Respect the user's "Download profile images" setting: even if the
|
||||||
|
// cache holds the URL we hide it when the toggle is off so the
|
||||||
|
// frontend doesn't render the <img> (which would still fetch from
|
||||||
|
// QRZ). Cheap to check per call — settings is in-memory after init.
|
||||||
|
if err == nil && r.ImageURL != "" {
|
||||||
|
if s, _ := a.GetLookupSettings(); !s.DownloadImages {
|
||||||
|
r.ImageURL = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenExternalURL opens a URL in the user's default browser. Wails ships
|
||||||
|
// runtime.BrowserOpenURL for exactly this — used by the QRZ.com icon
|
||||||
|
// next to the callsign field, the future Clublog/HamQTH shortcuts, etc.
|
||||||
|
func (a *App) OpenExternalURL(url string) error {
|
||||||
|
url = strings.TrimSpace(url)
|
||||||
|
if url == "" {
|
||||||
|
return fmt.Errorf("empty URL")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||||
|
return fmt.Errorf("only http(s) URLs allowed, got %q", url)
|
||||||
|
}
|
||||||
|
wruntime.BrowserOpenURL(a.ctx, url)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetLookupSettings returns current credentials and cache TTL.
|
// GetLookupSettings returns current credentials and cache TTL.
|
||||||
func (a *App) GetLookupSettings() (LookupSettings, error) {
|
func (a *App) GetLookupSettings() (LookupSettings, error) {
|
||||||
if a.settings == nil {
|
if a.settings == nil {
|
||||||
@@ -513,7 +539,7 @@ func (a *App) GetLookupSettings() (LookupSettings, error) {
|
|||||||
}
|
}
|
||||||
m, err := a.settings.GetMany(a.ctx,
|
m, err := a.settings.GetMany(a.ctx,
|
||||||
keyQRZUser, keyQRZPassword, keyHQUser, keyHQPassword,
|
keyQRZUser, keyQRZPassword, keyHQUser, keyHQPassword,
|
||||||
keyCacheTTL, keyLookupPrimary, keyLookupFailsafe)
|
keyCacheTTL, keyLookupPrimary, keyLookupFailsafe, keyLookupImages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return LookupSettings{}, err
|
return LookupSettings{}, err
|
||||||
}
|
}
|
||||||
@@ -528,6 +554,7 @@ func (a *App) GetLookupSettings() (LookupSettings, error) {
|
|||||||
HamQTHPassword: m[keyHQPassword],
|
HamQTHPassword: m[keyHQPassword],
|
||||||
Primary: m[keyLookupPrimary],
|
Primary: m[keyLookupPrimary],
|
||||||
Failsafe: m[keyLookupFailsafe],
|
Failsafe: m[keyLookupFailsafe],
|
||||||
|
DownloadImages: m[keyLookupImages] == "1",
|
||||||
CacheTTLDays: ttl,
|
CacheTTLDays: ttl,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -553,6 +580,7 @@ func (a *App) SaveLookupSettings(s LookupSettings) error {
|
|||||||
keyCacheTTL: strconv.Itoa(s.CacheTTLDays),
|
keyCacheTTL: strconv.Itoa(s.CacheTTLDays),
|
||||||
keyLookupPrimary: s.Primary,
|
keyLookupPrimary: s.Primary,
|
||||||
keyLookupFailsafe: s.Failsafe,
|
keyLookupFailsafe: s.Failsafe,
|
||||||
|
keyLookupImages: boolStr(s.DownloadImages),
|
||||||
} {
|
} {
|
||||||
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+49
-3
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AlertCircle, Antenna, CheckCircle2, Clock, Compass, Hash, Loader2, Lock,
|
AlertCircle, Antenna, CheckCircle2, Clock, Compass, ExternalLink, Hash, Loader2, Lock,
|
||||||
Maximize2, Minimize2, Pencil, RadioTower, RefreshCw, Send, Settings, Square, Trash2, Unlock, X,
|
Maximize2, Minimize2, Pencil, RadioTower, RefreshCw, Send, Settings, Square, Trash2, Unlock, X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig,
|
GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig,
|
||||||
RefreshCtyDat,
|
RefreshCtyDat,
|
||||||
RotatorGoTo, RotatorStop,
|
RotatorGoTo, RotatorStop,
|
||||||
|
OpenExternalURL,
|
||||||
GetCATSettings,
|
GetCATSettings,
|
||||||
} from '../wailsjs/go/main/App';
|
} from '../wailsjs/go/main/App';
|
||||||
import { EventsOn, EventsOff } from '../wailsjs/runtime/runtime';
|
import { EventsOn, EventsOff } from '../wailsjs/runtime/runtime';
|
||||||
@@ -555,6 +556,8 @@ export default function App() {
|
|||||||
|
|
||||||
function resetAutoFill() {
|
function resetAutoFill() {
|
||||||
setName(''); setQth(''); setCountry(''); setGrid('');
|
setName(''); setQth(''); setCountry(''); setGrid('');
|
||||||
|
setWb(null);
|
||||||
|
setLookupResult(null);
|
||||||
setDetails((d) => ({
|
setDetails((d) => ({
|
||||||
...d,
|
...d,
|
||||||
state: '', cnty: '', address: '',
|
state: '', cnty: '', address: '',
|
||||||
@@ -1010,6 +1013,21 @@ export default function App() {
|
|||||||
<div className="flex flex-col w-40">
|
<div className="flex flex-col w-40">
|
||||||
<Label className="mb-1 flex items-center gap-2 h-3.5">
|
<Label className="mb-1 flex items-center gap-2 h-3.5">
|
||||||
Callsign
|
Callsign
|
||||||
|
{callsign.trim() && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() => {
|
||||||
|
const c = callsign.trim().toUpperCase();
|
||||||
|
OpenExternalURL(`https://www.qrz.com/db/${encodeURIComponent(c)}`)
|
||||||
|
.catch((err) => setError(String(err?.message ?? err)));
|
||||||
|
}}
|
||||||
|
title="Open this callsign on QRZ.com"
|
||||||
|
className="inline-flex items-center justify-center size-3.5 rounded text-muted-foreground/60 hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink className="size-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{lookupBusy && <Badge variant="secondary" className="text-[9px] py-0 px-1.5 normal-case font-medium tracking-wider"><Loader2 className="size-2.5 mr-1 animate-spin" />Looking up…</Badge>}
|
{lookupBusy && <Badge variant="secondary" className="text-[9px] py-0 px-1.5 normal-case font-medium tracking-wider"><Loader2 className="size-2.5 mr-1 animate-spin" />Looking up…</Badge>}
|
||||||
{!lookupBusy && lookupResult && (
|
{!lookupBusy && lookupResult && (
|
||||||
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 text-[9px] py-0 px-1.5 normal-case font-medium tracking-wider" variant="outline">
|
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 text-[9px] py-0 px-1.5 normal-case font-medium tracking-wider" variant="outline">
|
||||||
@@ -1205,7 +1223,35 @@ export default function App() {
|
|||||||
else and let the user re-expand with the topbar toggle. */}
|
else and let the user re-expand with the topbar toggle. */}
|
||||||
{compact ? null : <>
|
{compact ? null : <>
|
||||||
{/* ===== BAND/SLOT GRID ===== */}
|
{/* ===== BAND/SLOT GRID ===== */}
|
||||||
<BandSlotGrid wb={wb} busy={wbBusy} currentBand={band} currentMode={mode} />
|
{/* QRZ profile picture sits next to the matrix when the user has
|
||||||
|
opted in (Settings → Lookup → Show QRZ profile pictures). The
|
||||||
|
backend returns image_url="" when the toggle is off, so we
|
||||||
|
don't need to re-check the setting here. */}
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<BandSlotGrid wb={wb} busy={wbBusy} currentBand={band} currentMode={mode} />
|
||||||
|
</div>
|
||||||
|
{lookupResult?.image_url && (
|
||||||
|
<a
|
||||||
|
href={lookupResult.image_url}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
OpenExternalURL(lookupResult.image_url!).catch((err) => setError(String(err?.message ?? err)));
|
||||||
|
}}
|
||||||
|
title="Open full-size on QRZ.com"
|
||||||
|
className="block shrink-0 rounded border border-border overflow-hidden hover:border-primary/60 transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={lookupResult.image_url}
|
||||||
|
alt={`${callsign} profile`}
|
||||||
|
className="block w-[160px] h-[120px] object-cover bg-muted/30"
|
||||||
|
loading="lazy"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ===== F2-F5 DETAILS ===== */}
|
{/* ===== F2-F5 DETAILS ===== */}
|
||||||
<DetailsPanel
|
<DetailsPanel
|
||||||
@@ -1340,7 +1386,7 @@ export default function App() {
|
|||||||
<td className="px-2.5 py-1.5 whitespace-nowrap border-b border-border/40">
|
<td className="px-2.5 py-1.5 whitespace-nowrap border-b border-border/40">
|
||||||
<span className="inline-block px-2 py-0.5 rounded font-mono text-[11px] font-semibold bg-emerald-100 text-emerald-700">{q.mode}</span>
|
<span className="inline-block px-2 py-0.5 rounded font-mono text-[11px] font-semibold bg-emerald-100 text-emerald-700">{q.mode}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-2.5 py-1.5 font-mono text-right whitespace-nowrap border-b border-border/40">{fmtFreq(q.freq_hz)}</td>
|
<td className="px-2.5 py-1.5 font-mono text-right whitespace-nowrap border-b border-border/40">{q.freq_hz ? fmtFreqDots(fmtFreq(q.freq_hz)) : ''}</td>
|
||||||
<td className="px-2.5 py-1.5 font-mono whitespace-nowrap border-b border-border/40">{q.rst_sent ?? ''}</td>
|
<td className="px-2.5 py-1.5 font-mono whitespace-nowrap border-b border-border/40">{q.rst_sent ?? ''}</td>
|
||||||
<td className="px-2.5 py-1.5 font-mono whitespace-nowrap border-b border-border/40">{q.rst_rcvd ?? ''}</td>
|
<td className="px-2.5 py-1.5 font-mono whitespace-nowrap border-b border-border/40">{q.rst_rcvd ?? ''}</td>
|
||||||
<td className="px-2.5 py-1.5 whitespace-nowrap border-b border-border/40">{q.name ?? ''}</td>
|
<td className="px-2.5 py-1.5 whitespace-nowrap border-b border-border/40">{q.name ?? ''}</td>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const TREE: TreeNode[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: 'group', label: 'Hardware Configuration', icon: Server, children: [
|
kind: 'group', label: 'Hardware Configuration', icon: Server, defaultOpen: true, children: [
|
||||||
{ kind: 'item', label: 'CAT interface (OmniRig)', id: 'cat' },
|
{ kind: 'item', label: 'CAT interface (OmniRig)', id: 'cat' },
|
||||||
{ kind: 'item', label: 'Rotator (PstRotator)', id: 'rotator' },
|
{ kind: 'item', label: 'Rotator (PstRotator)', id: 'rotator' },
|
||||||
{ kind: 'item', label: 'Antenna (Ultrabeam)', id: 'antenna', disabled: true },
|
{ kind: 'item', label: 'Antenna (Ultrabeam)', id: 'antenna', disabled: true },
|
||||||
@@ -225,6 +225,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
|||||||
qrz_user: '', qrz_password: '',
|
qrz_user: '', qrz_password: '',
|
||||||
hamqth_user: '', hamqth_password: '',
|
hamqth_user: '', hamqth_password: '',
|
||||||
primary: '', failsafe: '',
|
primary: '', failsafe: '',
|
||||||
|
download_images: false,
|
||||||
cache_ttl_days: 30,
|
cache_ttl_days: 30,
|
||||||
});
|
});
|
||||||
// Per-provider Test state — keeps the success/error feedback adjacent
|
// Per-provider Test state — keeps the success/error feedback adjacent
|
||||||
@@ -712,6 +713,24 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
|||||||
Failsafe is consulted only when the Primary returns no match or errors. Set both to none (uncheck) during contests to skip the network entirely.
|
Failsafe is consulted only when the Primary returns no match or errors. Set both to none (uncheck) during contests to skip the network entirely.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6 pt-4 border-t border-border">
|
||||||
|
<h3 className="text-sm font-semibold mb-2">Display</h3>
|
||||||
|
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||||
|
<Checkbox
|
||||||
|
checked={lookup.download_images}
|
||||||
|
onCheckedChange={(c) => setLookup((s) => ({ ...s, download_images: !!c }))}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Show QRZ profile pictures
|
||||||
|
<span className="block text-xs text-muted-foreground mt-0.5">
|
||||||
|
Display the photo from QRZ.com next to the worked-before matrix.
|
||||||
|
May noticeably slow lookups during busy contest days; turn off if you operate fast.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 pt-4 border-t border-border">
|
<div className="mt-6 pt-4 border-t border-border">
|
||||||
<h3 className="text-sm font-semibold mb-2">Cache</h3>
|
<h3 className="text-sm font-semibold mb-2">Cache</h3>
|
||||||
<p className="text-xs text-muted-foreground mb-3">
|
<p className="text-xs text-muted-foreground mb-3">
|
||||||
|
|||||||
Vendored
+2
@@ -53,6 +53,8 @@ export function LookupCallsign(arg1:string):Promise<lookup.Result>;
|
|||||||
|
|
||||||
export function OpenADIFFile():Promise<string>;
|
export function OpenADIFFile():Promise<string>;
|
||||||
|
|
||||||
|
export function OpenExternalURL(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function RefreshCtyDat():Promise<main.CtyDatInfo>;
|
export function RefreshCtyDat():Promise<main.CtyDatInfo>;
|
||||||
|
|
||||||
export function RotatorGoTo(arg1:number,arg2:number):Promise<void>;
|
export function RotatorGoTo(arg1:number,arg2:number):Promise<void>;
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ export function OpenADIFFile() {
|
|||||||
return window['go']['main']['App']['OpenADIFFile']();
|
return window['go']['main']['App']['OpenADIFFile']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function OpenExternalURL(arg1) {
|
||||||
|
return window['go']['main']['App']['OpenExternalURL'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function RefreshCtyDat() {
|
export function RefreshCtyDat() {
|
||||||
return window['go']['main']['App']['RefreshCtyDat']();
|
return window['go']['main']['App']['RefreshCtyDat']();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export namespace lookup {
|
|||||||
cont?: string;
|
cont?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
qsl_via?: string;
|
qsl_via?: string;
|
||||||
|
image_url?: string;
|
||||||
source: string;
|
source: string;
|
||||||
// Go type: time
|
// Go type: time
|
||||||
fetched_at: any;
|
fetched_at: any;
|
||||||
@@ -130,6 +131,7 @@ export namespace lookup {
|
|||||||
this.cont = source["cont"];
|
this.cont = source["cont"];
|
||||||
this.email = source["email"];
|
this.email = source["email"];
|
||||||
this.qsl_via = source["qsl_via"];
|
this.qsl_via = source["qsl_via"];
|
||||||
|
this.image_url = source["image_url"];
|
||||||
this.source = source["source"];
|
this.source = source["source"];
|
||||||
this.fetched_at = this.convertValues(source["fetched_at"], null);
|
this.fetched_at = this.convertValues(source["fetched_at"], null);
|
||||||
}
|
}
|
||||||
@@ -252,6 +254,7 @@ export namespace main {
|
|||||||
hamqth_password: string;
|
hamqth_password: string;
|
||||||
primary: string;
|
primary: string;
|
||||||
failsafe: string;
|
failsafe: string;
|
||||||
|
download_images: boolean;
|
||||||
cache_ttl_days: number;
|
cache_ttl_days: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
@@ -266,6 +269,7 @@ export namespace main {
|
|||||||
this.hamqth_password = source["hamqth_password"];
|
this.hamqth_password = source["hamqth_password"];
|
||||||
this.primary = source["primary"];
|
this.primary = source["primary"];
|
||||||
this.failsafe = source["failsafe"];
|
this.failsafe = source["failsafe"];
|
||||||
|
this.download_images = source["download_images"];
|
||||||
this.cache_ttl_days = source["cache_ttl_days"];
|
this.cache_ttl_days = source["cache_ttl_days"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- QRZ profile image URL — surfaced next to the worked-before matrix when
|
||||||
|
-- the user opts in via Settings → Callsign Lookup → Download profile images.
|
||||||
|
ALTER TABLE callsign_cache ADD COLUMN image_url TEXT;
|
||||||
+16
-12
@@ -33,7 +33,8 @@ type Result struct {
|
|||||||
Continent string `json:"cont,omitempty"`
|
Continent string `json:"cont,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
QSLVia string `json:"qsl_via,omitempty"`
|
QSLVia string `json:"qsl_via,omitempty"`
|
||||||
Source string `json:"source"` // "qrz", "hamqth", or "cache"
|
ImageURL string `json:"image_url,omitempty"` // profile picture URL (QRZ only for now)
|
||||||
|
Source string `json:"source"` // "qrz", "hamqth", or "cache"
|
||||||
FetchedAt time.Time `json:"fetched_at"`
|
FetchedAt time.Time `json:"fetched_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,21 +205,21 @@ func (c *Cache) SetTTL(ttl time.Duration) {
|
|||||||
func (c *Cache) Get(ctx context.Context, callsign string) (Result, bool) {
|
func (c *Cache) Get(ctx context.Context, callsign string) (Result, bool) {
|
||||||
row := c.db.QueryRowContext(ctx, `
|
row := c.db.QueryRowContext(ctx, `
|
||||||
SELECT callsign, name, qth, address, state, cnty, country, grid,
|
SELECT callsign, name, qth, address, state, cnty, country, grid,
|
||||||
lat, lon, dxcc, cqz, ituz, cont, email, qsl_via,
|
lat, lon, dxcc, cqz, ituz, cont, email, qsl_via, image_url,
|
||||||
source, fetched_at
|
source, fetched_at
|
||||||
FROM callsign_cache WHERE callsign = ?`, callsign)
|
FROM callsign_cache WHERE callsign = ?`, callsign)
|
||||||
var (
|
var (
|
||||||
r Result
|
r Result
|
||||||
name, qth, addr, state, cnty sql.NullString
|
name, qth, addr, state, cnty sql.NullString
|
||||||
country, grid, cont, email, qslVia sql.NullString
|
country, grid, cont, email, qslVia, image sql.NullString
|
||||||
src string
|
src string
|
||||||
dxcc, cqz, ituz sql.NullInt64
|
dxcc, cqz, ituz sql.NullInt64
|
||||||
lat, lon sql.NullFloat64
|
lat, lon sql.NullFloat64
|
||||||
fetched string
|
fetched string
|
||||||
)
|
)
|
||||||
if err := row.Scan(&r.Callsign, &name, &qth, &addr, &state, &cnty,
|
if err := row.Scan(&r.Callsign, &name, &qth, &addr, &state, &cnty,
|
||||||
&country, &grid, &lat, &lon,
|
&country, &grid, &lat, &lon,
|
||||||
&dxcc, &cqz, &ituz, &cont, &email, &qslVia,
|
&dxcc, &cqz, &ituz, &cont, &email, &qslVia, &image,
|
||||||
&src, &fetched); err != nil {
|
&src, &fetched); err != nil {
|
||||||
return Result{}, false
|
return Result{}, false
|
||||||
}
|
}
|
||||||
@@ -241,6 +242,7 @@ func (c *Cache) Get(ctx context.Context, callsign string) (Result, bool) {
|
|||||||
r.Continent = cont.String
|
r.Continent = cont.String
|
||||||
r.Email = email.String
|
r.Email = email.String
|
||||||
r.QSLVia = qslVia.String
|
r.QSLVia = qslVia.String
|
||||||
|
r.ImageURL = image.String
|
||||||
r.DXCC = int(dxcc.Int64)
|
r.DXCC = int(dxcc.Int64)
|
||||||
r.CQZ = int(cqz.Int64)
|
r.CQZ = int(cqz.Int64)
|
||||||
r.ITUZ = int(ituz.Int64)
|
r.ITUZ = int(ituz.Int64)
|
||||||
@@ -254,9 +256,9 @@ func (c *Cache) Put(ctx context.Context, r Result) error {
|
|||||||
_, err := c.db.ExecContext(ctx, `
|
_, err := c.db.ExecContext(ctx, `
|
||||||
INSERT INTO callsign_cache(callsign, name, qth, address, state, cnty,
|
INSERT INTO callsign_cache(callsign, name, qth, address, state, cnty,
|
||||||
country, grid, lat, lon,
|
country, grid, lat, lon,
|
||||||
dxcc, cqz, ituz, cont, email, qsl_via,
|
dxcc, cqz, ituz, cont, email, qsl_via, image_url,
|
||||||
source, fetched_at)
|
source, fetched_at)
|
||||||
VALUES(?,?,?,?,?,?, ?,?,?,?, ?,?,?,?,?,?, ?,
|
VALUES(?,?,?,?,?,?, ?,?,?,?, ?,?,?,?,?,?,?, ?,
|
||||||
strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
ON CONFLICT(callsign) DO UPDATE SET
|
ON CONFLICT(callsign) DO UPDATE SET
|
||||||
name = excluded.name, qth = excluded.qth, address = excluded.address,
|
name = excluded.name, qth = excluded.qth, address = excluded.address,
|
||||||
@@ -265,6 +267,7 @@ func (c *Cache) Put(ctx context.Context, r Result) error {
|
|||||||
lat = excluded.lat, lon = excluded.lon,
|
lat = excluded.lat, lon = excluded.lon,
|
||||||
dxcc = excluded.dxcc, cqz = excluded.cqz, ituz = excluded.ituz,
|
dxcc = excluded.dxcc, cqz = excluded.cqz, ituz = excluded.ituz,
|
||||||
cont = excluded.cont, email = excluded.email, qsl_via = excluded.qsl_via,
|
cont = excluded.cont, email = excluded.email, qsl_via = excluded.qsl_via,
|
||||||
|
image_url = excluded.image_url,
|
||||||
source = excluded.source, fetched_at = excluded.fetched_at`,
|
source = excluded.source, fetched_at = excluded.fetched_at`,
|
||||||
r.Callsign, nullable(r.Name), nullable(r.QTH), nullable(r.Address),
|
r.Callsign, nullable(r.Name), nullable(r.QTH), nullable(r.Address),
|
||||||
nullable(r.State), nullable(r.County),
|
nullable(r.State), nullable(r.County),
|
||||||
@@ -272,6 +275,7 @@ func (c *Cache) Put(ctx context.Context, r Result) error {
|
|||||||
nullableFloat(r.Lat), nullableFloat(r.Lon),
|
nullableFloat(r.Lat), nullableFloat(r.Lon),
|
||||||
nullableInt(r.DXCC), nullableInt(r.CQZ), nullableInt(r.ITUZ),
|
nullableInt(r.DXCC), nullableInt(r.CQZ), nullableInt(r.ITUZ),
|
||||||
nullable(r.Continent), nullable(r.Email), nullable(r.QSLVia),
|
nullable(r.Continent), nullable(r.Email), nullable(r.QSLVia),
|
||||||
|
nullable(r.ImageURL),
|
||||||
r.Source,
|
r.Source,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ func (q *QRZ) fetch(ctx context.Context, sessionKey, callsign string) (Result, e
|
|||||||
Continent: strings.ToUpper(c.Continent),
|
Continent: strings.ToUpper(c.Continent),
|
||||||
Email: c.Email,
|
Email: c.Email,
|
||||||
QSLVia: c.QSLMgr,
|
QSLVia: c.QSLMgr,
|
||||||
|
ImageURL: strings.TrimSpace(c.Image),
|
||||||
}
|
}
|
||||||
r.Lat, _ = strconv.ParseFloat(c.Lat, 64)
|
r.Lat, _ = strconv.ParseFloat(c.Lat, 64)
|
||||||
r.Lon, _ = strconv.ParseFloat(c.Lon, 64)
|
r.Lon, _ = strconv.ParseFloat(c.Lon, 64)
|
||||||
@@ -184,6 +185,7 @@ type qrzCallsign struct {
|
|||||||
Continent string `xml:"cont"`
|
Continent string `xml:"cont"`
|
||||||
Email string `xml:"email"`
|
Email string `xml:"email"`
|
||||||
QSLMgr string `xml:"qslmgr"`
|
QSLMgr string `xml:"qslmgr"`
|
||||||
|
Image string `xml:"image"` // direct URL to the profile picture (subscribers only on QRZ)
|
||||||
}
|
}
|
||||||
|
|
||||||
// composeQRZAddress builds a multi-line postal address from QRZ's separate
|
// composeQRZAddress builds a multi-line postal address from QRZ's separate
|
||||||
|
|||||||
Reference in New Issue
Block a user