diff --git a/alertproviders/parksnpeaks.py b/alertproviders/parksnpeaks.py index ad288eb..010d99c 100644 --- a/alertproviders/parksnpeaks.py +++ b/alertproviders/parksnpeaks.py @@ -33,18 +33,24 @@ class ParksNPeaks(HTTPAlertProvider): start_time = datetime.strptime(source_alert["alTime"], "%Y-%m-%d %H:%M:%S").replace( tzinfo=pytz.UTC).timestamp() + sigrefs = [] + # PnP can give us an alert of class "QRP" which is the only one that's not a real SIG in Spothole's list, + # so mask this out if we got it. + if sig != "QRP": + sigrefs = [SIGRef(id=sig_ref, sig=sig, name=sig_ref_name)] + # Convert to our alert format alert = Alert(source=self.name, source_id=source_alert["alID"], dx_calls=[source_alert["CallSign"].upper()], freqs_modes=source_alert["Freq"] + " " + source_alert["MODE"], comment=source_alert["Comments"], - sig_refs=[SIGRef(id=sig_ref, sig=sig, name=sig_ref_name)], + sig_refs=sigrefs, start_time=start_time, is_dxpedition=False) # Log a warning for the developer if PnP gives us an unknown programme we've never seen before - if sig and sig not in ["POTA", "SOTA", "WWFF", "SiOTA", "ZLOTA", "KRMNPA"]: + if sig and sig not in ["POTA", "SOTA", "WWFF", "SiOTA", "ZLOTA", "KRMNPA", "LLOTA", "QRP"]: logging.warning("PNP alert found with sig " + sig + ", developer needs to add support for this!") # If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to diff --git a/core/cache_utils.py b/core/cache_utils.py index 3093c2f..2ec492c 100644 --- a/core/cache_utils.py +++ b/core/cache_utils.py @@ -1,3 +1,4 @@ +import threading from datetime import timedelta from requests_cache import CachedSession @@ -5,6 +6,19 @@ from requests_cache import CachedSession # Cache for "semi-static" data such as the locations of parks, CSVs of reference lists, etc. # This has an expiry time of 30 days, so will re-request from the source after that amount # of time has passed. This is used throughout Spothole to cache data that does not change -# rapidly. -SEMI_STATIC_URL_DATA_CACHE = CachedSession("cache/semi_static_url_data_cache", - expire_after=timedelta(days=30)) +# rapidly. The ThreadSafeSession construct here protects it against some multithreading +# contention weirdness we sometimes used to see on startup where the cache was hammered +# pretty hard. +_session = CachedSession("cache/semi_static_url_data_cache", expire_after=timedelta(days=30)) +_lock = threading.Lock() + + +class _ThreadSafeSession: + """Wraps CachedSession with a lock to prevent concurrent SQLite access across threads.""" + + def get(self, *args, **kwargs): + with _lock: + return _session.get(*args, **kwargs) + + +SEMI_STATIC_URL_DATA_CACHE = _ThreadSafeSession() diff --git a/core/sig_utils.py b/core/sig_utils.py index 4608659..aacd246 100644 --- a/core/sig_utils.py +++ b/core/sig_utils.py @@ -29,7 +29,10 @@ def populate_sig_ref_info(sig_ref): ref_id = sig_ref.id try: if sig.upper() == "POTA": - data = SEMI_STATIC_URL_DATA_CACHE.get("https://api.pota.app/park/" + ref_id, headers=HTTP_HEADERS).json() + response = SEMI_STATIC_URL_DATA_CACHE.get("https://api.pota.app/park/" + ref_id, headers=HTTP_HEADERS) + if not response.ok: + logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id) + data = response.json() if response.ok else None if data: fullname = str(data["name"]) if "name" in data else None if fullname and "parktypeDesc" in data and data["parktypeDesc"] != "": @@ -40,8 +43,11 @@ def populate_sig_ref_info(sig_ref): sig_ref.latitude = data["latitude"] if "latitude" in data else None sig_ref.longitude = data["longitude"] if "longitude" in data else None elif sig.upper() == "SOTA": - data = SEMI_STATIC_URL_DATA_CACHE.get("https://api-db2.sota.org.uk/api/summits/" + ref_id, - headers=HTTP_HEADERS).json() + response = SEMI_STATIC_URL_DATA_CACHE.get("https://api-db2.sota.org.uk/api/summits/" + ref_id, + headers=HTTP_HEADERS) + if not response.ok: + logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id) + data = response.json() if response.ok else None if data: sig_ref.name = data["name"] if "name" in data else None sig_ref.url = "https://www.sotadata.org.uk/en/summit/" + ref_id @@ -50,8 +56,11 @@ def populate_sig_ref_info(sig_ref): sig_ref.longitude = data["longitude"] if "longitude" in data else None sig_ref.activation_score = data["points"] if "points" in data else None elif sig.upper() == "WWBOTA": - data = SEMI_STATIC_URL_DATA_CACHE.get("https://api.wwbota.org/bunkers/" + ref_id, - headers=HTTP_HEADERS).json() + response = SEMI_STATIC_URL_DATA_CACHE.get("https://api.wwbota.org/bunkers/" + ref_id, + headers=HTTP_HEADERS) + if not response.ok: + logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id) + data = response.json() if response.ok else None if data: sig_ref.name = data["name"] if "name" in data else None sig_ref.url = "https://bunkerwiki.org/?s=" + ref_id if ref_id.startswith("B/G") else None @@ -59,8 +68,11 @@ def populate_sig_ref_info(sig_ref): sig_ref.latitude = data["lat"] if "lat" in data else None sig_ref.longitude = data["long"] if "long" in data else None elif sig.upper() == "GMA" or sig.upper() == "ARLHS" or sig.upper() == "ILLW" or sig.upper() == "WCA" or sig.upper() == "MOTA" or sig.upper() == "IOTA": - data = SEMI_STATIC_URL_DATA_CACHE.get("https://www.cqgma.org/api/ref/?" + ref_id, - headers=HTTP_HEADERS).json() + response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.cqgma.org/api/ref/?" + ref_id, + headers=HTTP_HEADERS) + if not response.ok: + logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id) + data = response.json() if response.ok else None if data: sig_ref.name = data["name"] if "name" in data else None sig_ref.url = "https://www.cqgma.org/zinfo.php?ref=" + ref_id @@ -68,9 +80,12 @@ def populate_sig_ref_info(sig_ref): sig_ref.latitude = data["latitude"] if "latitude" in data else None sig_ref.longitude = data["longitude"] if "longitude" in data else None elif sig.upper() == "WWFF": - wwff_csv_data = SEMI_STATIC_URL_DATA_CACHE.get("https://wwff.co/wwff-data/wwff_directory.csv", + wwff_response = SEMI_STATIC_URL_DATA_CACHE.get("https://wwff.co/wwff-data/wwff_directory.csv", headers=HTTP_HEADERS) - wwff_index = {row["reference"]: row for row in csv.DictReader(wwff_csv_data.content.decode().splitlines())} + if not wwff_response.ok: + logging.warning("HTTP %d looking up %s ref %s", wwff_response.status_code, sig, ref_id) + return sig_ref + wwff_index = {row["reference"]: row for row in csv.DictReader(wwff_response.content.decode().splitlines())} row = wwff_index.get(ref_id) if row: sig_ref.name = row["name"] if "name" in row else None @@ -79,10 +94,13 @@ def populate_sig_ref_info(sig_ref): sig_ref.latitude = float(row["latitude"]) if "latitude" in row and row["latitude"] != "-" else None sig_ref.longitude = float(row["longitude"]) if "longitude" in row and row["longitude"] != "-" else None elif sig.upper() == "SIOTA": - siota_csv_data = SEMI_STATIC_URL_DATA_CACHE.get("https://www.silosontheair.com/data/silos.csv", + siota_response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.silosontheair.com/data/silos.csv", headers=HTTP_HEADERS) + if not siota_response.ok: + logging.warning("HTTP %d looking up %s ref %s", siota_response.status_code, sig, ref_id) + return sig_ref siota_index = {row["SILO_CODE"]: row for row in - csv.DictReader(siota_csv_data.content.decode().splitlines())} + csv.DictReader(siota_response.content.decode().splitlines())} row = siota_index.get(ref_id) if row: sig_ref.name = row["NAME"] if "NAME" in row else None @@ -90,10 +108,13 @@ def populate_sig_ref_info(sig_ref): sig_ref.latitude = float(row["LAT"]) if "LAT" in row else None sig_ref.longitude = float(row["LNG"]) if "LNG" in row else None elif sig.upper() == "WOTA": - data = SEMI_STATIC_URL_DATA_CACHE.get("https://www.wota.org.uk/mapping/data/summits.json", - headers=HTTP_HEADERS).json() + response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.wota.org.uk/mapping/data/summits.json", + headers=HTTP_HEADERS) + if not response.ok: + logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id) + data = response.json() if response.ok else None if data: - for feature in data["features"]: + for feature in data.get("features", []): if feature["properties"]["wotaId"] == ref_id: sig_ref.name = feature["properties"]["title"] # Fudge WOTA URLs. Outlying fell (LDO) URLs don't match their ID numbers but require 214 to be @@ -107,8 +128,11 @@ def populate_sig_ref_info(sig_ref): sig_ref.longitude = feature["geometry"]["coordinates"][0] break elif sig.upper() == "ZLOTA": - data = SEMI_STATIC_URL_DATA_CACHE.get("https://ontheair.nz/assets/assets.json", headers=HTTP_HEADERS).json() - if data: + response = SEMI_STATIC_URL_DATA_CACHE.get("https://ontheair.nz/assets/assets.json", headers=HTTP_HEADERS) + if not response.ok: + logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id) + data = response.json() if response.ok else None + if isinstance(data, list): for asset in data: if asset["code"] == ref_id: sig_ref.name = asset["name"] @@ -125,9 +149,12 @@ def populate_sig_ref_info(sig_ref): sig_ref.name = sig_ref.id sig_ref.url = "https://www.beachesontheair.com/beaches/" + sig_ref.name.lower().replace(" ", "-") elif sig.upper() == "LLOTA": - data = SEMI_STATIC_URL_DATA_CACHE.get("https://llota.app/api/public/references", - headers=HTTP_HEADERS).json() - if data: + response = SEMI_STATIC_URL_DATA_CACHE.get("https://llota.app/api/public/references", + headers=HTTP_HEADERS) + if not response.ok: + logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id) + data = response.json() if response.ok else None + if isinstance(data, list): for ref in data: if ref["reference_code"] == ref_id: sig_ref.name = str(ref["name"]) @@ -161,8 +188,8 @@ def populate_sig_ref_info(sig_ref): sig_ref.longitude = ll[1] except: logging.debug("Invalid lat/lon received for reference") - except: - logging.warning("Failed to look up sig_ref info for " + sig + " ref " + ref_id + ".") + except Exception: + logging.warning("Failed to look up sig_ref info for " + sig + " ref " + ref_id, exc_info=True) return sig_ref diff --git a/spotproviders/parksnpeaks.py b/spotproviders/parksnpeaks.py index 6beac64..79578d8 100644 --- a/spotproviders/parksnpeaks.py +++ b/spotproviders/parksnpeaks.py @@ -61,7 +61,7 @@ class ParksNPeaks(HTTPSpotProvider): sig_refs[0].name = source_spot["actLocation"] # Log a warning for the developer if PnP gives us an unknown programme we've never seen before - if sig not in ["POTA", "SOTA", "WWFF", "SIOTA", "ZLOTA", "KRMNPA"]: + if sig not in ["POTA", "SOTA", "WWFF", "SIOTA", "ZLOTA", "KRMNPA", "LLOTA"]: logging.warning("PNP spot found with sig " + sig + ", developer needs to add support for this!") # Add new spot to the list diff --git a/templates/add_spot.html b/templates/add_spot.html index 8e99244..ddcd446 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -76,7 +76,7 @@ - + diff --git a/templates/alerts.html b/templates/alerts.html index 18b0dfd..fef1ee6 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -75,7 +75,7 @@ - + diff --git a/templates/bands.html b/templates/bands.html index 3de0646..50fce7a 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -77,8 +77,8 @@ - - + + diff --git a/templates/base.html b/templates/base.html index d70aacc..36341da 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -10,10 +10,10 @@ - - - - + + + + {% end %} {% block body %}