mirror of
https://github.com/smittix/intercept.git
synced 2026-07-17 05:48:11 -07:00
Merge pull request #210 from smittix/fix/wefax-stations-missing-in-docker
fix(wefax): include station JSON in Docker image and handle missing file
This commit is contained in:
@@ -42,7 +42,6 @@ tasks/
|
|||||||
instance/
|
instance/
|
||||||
|
|
||||||
# data/ is a Python package — only exclude non-code files
|
# data/ is a Python package — only exclude non-code files
|
||||||
data/*.json
|
|
||||||
data/*.csv
|
data/*.csv
|
||||||
data/*.db
|
data/*.db
|
||||||
|
|
||||||
|
|||||||
+103
-95
@@ -3,36 +3,44 @@
|
|||||||
Loads and caches station data from data/wefax_stations.json. Provides
|
Loads and caches station data from data/wefax_stations.json. Provides
|
||||||
lookup by callsign and current-broadcast filtering based on UTC time.
|
lookup by callsign and current-broadcast filtering based on UTC time.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timezone
|
import logging
|
||||||
from pathlib import Path
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
_stations_cache: list[dict] | None = None
|
_stations_cache: list[dict] | None = None
|
||||||
_stations_by_callsign: dict[str, dict] = {}
|
_stations_by_callsign: dict[str, dict] = {}
|
||||||
_VALID_FREQUENCY_REFERENCES = {'auto', 'carrier', 'dial'}
|
_VALID_FREQUENCY_REFERENCES = {"auto", "carrier", "dial"}
|
||||||
WEFAX_USB_ALIGNMENT_OFFSET_KHZ = 1.9
|
WEFAX_USB_ALIGNMENT_OFFSET_KHZ = 1.9
|
||||||
|
|
||||||
_STATIONS_PATH = Path(__file__).resolve().parent.parent / 'data' / 'wefax_stations.json'
|
_STATIONS_PATH = Path(__file__).resolve().parent.parent / "data" / "wefax_stations.json"
|
||||||
|
|
||||||
|
|
||||||
def load_stations() -> list[dict]:
|
def load_stations() -> list[dict]:
|
||||||
"""Load all WeFax stations from JSON, caching on first call."""
|
"""Load all WeFax stations from JSON, caching on first call."""
|
||||||
global _stations_cache, _stations_by_callsign
|
global _stations_cache, _stations_by_callsign
|
||||||
|
|
||||||
if _stations_cache is not None:
|
if _stations_cache is not None:
|
||||||
return _stations_cache
|
return _stations_cache
|
||||||
|
|
||||||
with open(_STATIONS_PATH) as f:
|
if not _STATIONS_PATH.exists():
|
||||||
data = json.load(f)
|
log.warning("wefax_stations.json not found at %s", _STATIONS_PATH)
|
||||||
|
_stations_cache = []
|
||||||
_stations_cache = data.get('stations', [])
|
return _stations_cache
|
||||||
_stations_by_callsign = {s['callsign']: s for s in _stations_cache}
|
|
||||||
return _stations_cache
|
with open(_STATIONS_PATH) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
_stations_cache = data.get("stations", [])
|
||||||
|
_stations_by_callsign = {s["callsign"]: s for s in _stations_cache}
|
||||||
|
return _stations_cache
|
||||||
|
|
||||||
|
|
||||||
def get_station(callsign: str) -> dict | None:
|
def get_station(callsign: str) -> dict | None:
|
||||||
"""Get a single station by callsign."""
|
"""Get a single station by callsign."""
|
||||||
load_stations()
|
load_stations()
|
||||||
@@ -41,38 +49,38 @@ def get_station(callsign: str) -> dict | None:
|
|||||||
|
|
||||||
def _normalize_frequency_reference(value: str | None) -> str:
|
def _normalize_frequency_reference(value: str | None) -> str:
|
||||||
"""Normalize and validate frequency reference token."""
|
"""Normalize and validate frequency reference token."""
|
||||||
reference = str(value or 'auto').strip().lower()
|
reference = str(value or "auto").strip().lower()
|
||||||
if reference not in _VALID_FREQUENCY_REFERENCES:
|
if reference not in _VALID_FREQUENCY_REFERENCES:
|
||||||
choices = ', '.join(sorted(_VALID_FREQUENCY_REFERENCES))
|
choices = ", ".join(sorted(_VALID_FREQUENCY_REFERENCES))
|
||||||
raise ValueError(f'frequency_reference must be one of: {choices}')
|
raise ValueError(f"frequency_reference must be one of: {choices}")
|
||||||
return reference
|
return reference
|
||||||
|
|
||||||
|
|
||||||
def _station_frequency_reference(station: dict, listed_frequency_khz: float) -> str:
|
def _station_frequency_reference(station: dict, listed_frequency_khz: float) -> str:
|
||||||
"""Infer whether a station frequency entry is carrier or already USB dial."""
|
"""Infer whether a station frequency entry is carrier or already USB dial."""
|
||||||
for entry in station.get('frequencies', []):
|
for entry in station.get("frequencies", []):
|
||||||
try:
|
try:
|
||||||
entry_khz = float(entry.get('khz'))
|
entry_khz = float(entry.get("khz"))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
continue
|
continue
|
||||||
if abs(entry_khz - listed_frequency_khz) > 0.001:
|
if abs(entry_khz - listed_frequency_khz) > 0.001:
|
||||||
continue
|
continue
|
||||||
entry_ref = str(entry.get('reference', '')).strip().lower()
|
entry_ref = str(entry.get("reference", "")).strip().lower()
|
||||||
if entry_ref in ('carrier', 'dial'):
|
if entry_ref in ("carrier", "dial"):
|
||||||
return entry_ref
|
return entry_ref
|
||||||
|
|
||||||
station_ref = str(station.get('frequency_reference', '')).strip().lower()
|
station_ref = str(station.get("frequency_reference", "")).strip().lower()
|
||||||
if station_ref in ('carrier', 'dial'):
|
if station_ref in ("carrier", "dial"):
|
||||||
return station_ref
|
return station_ref
|
||||||
|
|
||||||
# Most published marine WeFax channel lists are carrier frequencies.
|
# Most published marine WeFax channel lists are carrier frequencies.
|
||||||
return 'carrier'
|
return "carrier"
|
||||||
|
|
||||||
|
|
||||||
def resolve_tuning_frequency_khz(
|
def resolve_tuning_frequency_khz(
|
||||||
listed_frequency_khz: float,
|
listed_frequency_khz: float,
|
||||||
station_callsign: str = '',
|
station_callsign: str = "",
|
||||||
frequency_reference: str = 'auto',
|
frequency_reference: str = "auto",
|
||||||
) -> tuple[float, str, bool]:
|
) -> tuple[float, str, bool]:
|
||||||
"""Resolve listed frequency to the actual USB dial frequency.
|
"""Resolve listed frequency to the actual USB dial frequency.
|
||||||
|
|
||||||
@@ -86,75 +94,75 @@ def resolve_tuning_frequency_khz(
|
|||||||
"""
|
"""
|
||||||
listed = float(listed_frequency_khz)
|
listed = float(listed_frequency_khz)
|
||||||
if listed <= 0:
|
if listed <= 0:
|
||||||
raise ValueError('frequency_khz must be greater than zero')
|
raise ValueError("frequency_khz must be greater than zero")
|
||||||
|
|
||||||
requested_ref = _normalize_frequency_reference(frequency_reference)
|
requested_ref = _normalize_frequency_reference(frequency_reference)
|
||||||
resolved_ref = requested_ref
|
resolved_ref = requested_ref
|
||||||
|
|
||||||
if requested_ref == 'auto':
|
if requested_ref == "auto":
|
||||||
station = get_station(station_callsign) if station_callsign else None
|
station = get_station(station_callsign) if station_callsign else None
|
||||||
if station:
|
if station:
|
||||||
resolved_ref = _station_frequency_reference(station, listed)
|
resolved_ref = _station_frequency_reference(station, listed)
|
||||||
else:
|
else:
|
||||||
# For ad-hoc frequencies (no station metadata), treat input as dial.
|
# For ad-hoc frequencies (no station metadata), treat input as dial.
|
||||||
resolved_ref = 'dial'
|
resolved_ref = "dial"
|
||||||
|
|
||||||
if resolved_ref == 'carrier':
|
if resolved_ref == "carrier":
|
||||||
tuned = round(listed - WEFAX_USB_ALIGNMENT_OFFSET_KHZ, 3)
|
tuned = round(listed - WEFAX_USB_ALIGNMENT_OFFSET_KHZ, 3)
|
||||||
if tuned <= 0:
|
if tuned <= 0:
|
||||||
raise ValueError('frequency_khz too low after USB alignment offset')
|
raise ValueError("frequency_khz too low after USB alignment offset")
|
||||||
return tuned, resolved_ref, True
|
return tuned, resolved_ref, True
|
||||||
|
|
||||||
return listed, resolved_ref, False
|
return listed, resolved_ref, False
|
||||||
|
|
||||||
|
|
||||||
def get_current_broadcasts(callsign: str) -> list[dict]:
|
def get_current_broadcasts(callsign: str) -> list[dict]:
|
||||||
"""Return schedule entries closest to the current UTC time.
|
"""Return schedule entries closest to the current UTC time.
|
||||||
|
|
||||||
Returns up to 3 entries: the most recent past broadcast and the
|
Returns up to 3 entries: the most recent past broadcast and the
|
||||||
next two upcoming ones, annotated with ``minutes_until`` or
|
next two upcoming ones, annotated with ``minutes_until`` or
|
||||||
``minutes_ago`` relative to now.
|
``minutes_ago`` relative to now.
|
||||||
"""
|
"""
|
||||||
station = get_station(callsign)
|
station = get_station(callsign)
|
||||||
if not station:
|
if not station:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
current_minutes = now.hour * 60 + now.minute
|
current_minutes = now.hour * 60 + now.minute
|
||||||
|
|
||||||
schedule = station.get('schedule', [])
|
schedule = station.get("schedule", [])
|
||||||
if not schedule:
|
if not schedule:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Convert schedule times to minutes-since-midnight for comparison
|
# Convert schedule times to minutes-since-midnight for comparison
|
||||||
entries: list[tuple[int, dict]] = []
|
entries: list[tuple[int, dict]] = []
|
||||||
for entry in schedule:
|
for entry in schedule:
|
||||||
parts = entry['utc'].split(':')
|
parts = entry["utc"].split(":")
|
||||||
mins = int(parts[0]) * 60 + int(parts[1])
|
mins = int(parts[0]) * 60 + int(parts[1])
|
||||||
entries.append((mins, entry))
|
entries.append((mins, entry))
|
||||||
entries.sort(key=lambda x: x[0])
|
entries.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
# Find closest entries relative to now
|
# Find closest entries relative to now
|
||||||
results = []
|
results = []
|
||||||
for mins, entry in entries:
|
for mins, entry in entries:
|
||||||
diff = mins - current_minutes
|
diff = mins - current_minutes
|
||||||
# Wrap around midnight
|
# Wrap around midnight
|
||||||
if diff < -720:
|
if diff < -720:
|
||||||
diff += 1440
|
diff += 1440
|
||||||
elif diff > 720:
|
elif diff > 720:
|
||||||
diff -= 1440
|
diff -= 1440
|
||||||
|
|
||||||
annotated = dict(entry)
|
annotated = dict(entry)
|
||||||
if diff >= 0:
|
if diff >= 0:
|
||||||
annotated['minutes_until'] = diff
|
annotated["minutes_until"] = diff
|
||||||
else:
|
else:
|
||||||
annotated['minutes_ago'] = abs(diff)
|
annotated["minutes_ago"] = abs(diff)
|
||||||
annotated['_sort_key'] = abs(diff)
|
annotated["_sort_key"] = abs(diff)
|
||||||
results.append(annotated)
|
results.append(annotated)
|
||||||
|
|
||||||
results.sort(key=lambda x: x['_sort_key'])
|
results.sort(key=lambda x: x["_sort_key"])
|
||||||
|
|
||||||
# Return 3 nearest entries, clean up sort key
|
# Return 3 nearest entries, clean up sort key
|
||||||
for r in results:
|
for r in results:
|
||||||
r.pop('_sort_key', None)
|
r.pop("_sort_key", None)
|
||||||
return results[:3]
|
return results[:3]
|
||||||
|
|||||||
Reference in New Issue
Block a user