From 1b62d4742bac761814c939617a5aedffc02cbc20 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Mon, 31 Aug 2026 21:54:13 +0100 Subject: [PATCH] Support PGA. Closes #135 --- config-example.yml | 3 ++ core/constants.py | 9 ++++++ core/utils.py | 4 +-- providers/sigrefdata/pga.py | 57 +++++++++++++++++++++++++++++++++++++ spothole.py | 4 +++ static/apidocs/openapi.yml | 4 ++- templates/about.html | 2 +- templates/add_spot.html | 2 +- templates/alerts.html | 2 +- templates/bands.html | 4 +-- templates/base.html | 10 +++---- templates/conditions.html | 2 +- templates/map.html | 4 +-- templates/spots.html | 4 +-- templates/status.html | 2 +- 15 files changed, 94 insertions(+), 19 deletions(-) create mode 100644 providers/sigrefdata/pga.py diff --git a/config-example.yml b/config-example.yml index 0c5688f..e9be2ab 100644 --- a/config-example.yml +++ b/config-example.yml @@ -261,6 +261,9 @@ sig_ref_data_providers: - class: "COTA" enabled: true + - class: "PGA" + enabled: true + - class: "Toilets" enabled: true diff --git a/core/constants.py b/core/constants.py index 7397c6f..0bcf39a 100644 --- a/core/constants.py +++ b/core/constants.py @@ -242,6 +242,15 @@ SIGS = [ region_flag="🇩🇪", refs_globally_unique=False ), + SIG( + name="PGA", + comment_names=["PGA"], + description="Polish Gmina Award", + ref_regex=r"[A-Z]{2}[0-9]{2}", + icon="fa-g", + region_flag="🇵🇱", + refs_globally_unique=False + ), SIG( name="Toilets", comment_names=[], diff --git a/core/utils.py b/core/utils.py index 11ca7a5..f0a4ab2 100644 --- a/core/utils.py +++ b/core/utils.py @@ -31,10 +31,10 @@ def infer_mode_from_comment(comment): """Infer a mode from the comment""" for mode in ALL_MODES: - if re.match(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE): + if re.search(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE): return mode for mode in MODE_ALIASES: - if re.match(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE): + if re.search(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE): return MODE_ALIASES[mode] return None diff --git a/providers/sigrefdata/pga.py b/providers/sigrefdata/pga.py new file mode 100644 index 0000000..d20bb04 --- /dev/null +++ b/providers/sigrefdata/pga.py @@ -0,0 +1,57 @@ +from time import sleep + +from bs4 import BeautifulSoup + +from data.sig_ref import SIGRef +from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider + + +class PGA(FileDownloadSIGRefDataProvider): + """SIG ref data provider for Polish Gmina Award""" + + POLL_INTERVAL_DAYS = 30 + SIG = "PGA" + DATA_URL = "http://www.spga.pl/lista_pga2.php" + + def __init__(self, provider_config): + super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS) + + def _http_response_to_data(self, http_response): + new_data = [] + soup = BeautifulSoup(http_response.text, "html.parser") + + # Iterate through tables in the page + for table in soup.find_all("table"): + header_cells = table.find_all(["th", "td"], limit=10) + header_texts = [c.get_text(strip=True) for c in header_cells] + + # If it has "PGA" and "Nazwa" in the header, it's the main data table + if any("PGA" in t for t in header_texts) and any("Nazwa" in t for t in header_texts): + # Iterate through all rows except the first + rows = table.find_all("tr") + for row in rows[1:]: + cells = row.find_all("td") + ref_id = cells[0].get_text(strip=True) + name = cells[1].get_text(strip=True) + if not ref_id: + continue + + new_data.append( + SIGRef( + sig=self.SIG, + id=ref_id, + name=name, + ref_type="Region", + ) + ) + + # Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest + # of the data in this case + if self._stop_event.is_set(): + break + + # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time + # is available for other threads e.g. the web server. + sleep(0.001) + + return new_data diff --git a/spothole.py b/spothole.py index 64ef753..9899c98 100644 --- a/spothole.py +++ b/spothole.py @@ -10,6 +10,7 @@ from core.constants import SOFTWARE_VERSION from core.data_providers import DATA_PROVIDERS from core.data_store import DATA_STORE from core.status_reporter import StatusReporter +from core.utils import infer_mode_from_comment from server.webserver import WEB_SERVER logger = logging.getLogger(__name__) @@ -28,6 +29,9 @@ def shutdown(_signum=None, _frame=None): # Main function if __name__ == "__main__": + result = infer_mode_from_comment("Test FT8") + print(result) + # Set up logging root = logging.getLogger() root.setLevel(LOG_LEVEL) diff --git a/static/apidocs/openapi.yml b/static/apidocs/openapi.yml index 6bf69a6..4d2769e 100644 --- a/static/apidocs/openapi.yml +++ b/static/apidocs/openapi.yml @@ -17,7 +17,7 @@ info: ### 2.1 - * Added DTMBA, FEA and BIWOTA SIGs + * Added DTMBA, FEA, BIWOTA, COTA & PGA SIGs * Removed the distinction between LSB & USB (both will now show as SSB) and between the various digital voice modes, which will now show as DV. * Added `icon`, `region_flag` and `refs_globally_unique` to SIG information @@ -861,6 +861,8 @@ components: - FEA - DTMBA - BIWOTA + - COTA + - PGA - Toilets example: POTA diff --git a/templates/about.html b/templates/about.html index a387a36..3f2f346 100644 --- a/templates/about.html +++ b/templates/about.html @@ -104,7 +104,7 @@ Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air (LLOTA), Towers on the Air, Tiles on the Air, Worked All Britain (WAB), Worked All Ireland (WAI), el Diploma Municipios de España (DME), el Diploma Faros de España (FEA), il Diploma Teatri Musei e Belle Arti (DTMBA), British Inland Waterways on the Air - (BIWOTA) and Toilets on the Air.

+ (BIWOTA), Castles on the Air (COTA), Polish Gmina Award (PGA), and Toilets on the Air.

As of the time of writing in August 2026, I think Spothole captures most outdoor radio programmes that have a defined, downloadable reference list, and almost certainly those that have a spotting/alerting API. If you know of one I've missed, please let me know!

diff --git a/templates/add_spot.html b/templates/add_spot.html index 6f0eb8c..3c00a89 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 02d39be..57061bd 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -84,7 +84,7 @@ - + diff --git a/templates/bands.html b/templates/bands.html index 39ce6c5..b4a7bd9 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -76,8 +76,8 @@ - - + + diff --git a/templates/base.html b/templates/base.html index 1d2a93e..2e3fd20 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -15,10 +15,10 @@ window.fetchEventSource = fetchEventSource; - - - - + + + + {% end %} {% block body %}
diff --git a/templates/conditions.html b/templates/conditions.html index 49fef8a..af6086f 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -284,7 +284,7 @@
- + diff --git a/templates/map.html b/templates/map.html index 104a2f9..d28d2a8 100644 --- a/templates/map.html +++ b/templates/map.html @@ -113,8 +113,8 @@ const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}"; - - + + diff --git a/templates/spots.html b/templates/spots.html index 6a747a9..94a5ef1 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -113,8 +113,8 @@ - - + + diff --git a/templates/status.html b/templates/status.html index 7a1c69a..29310e3 100644 --- a/templates/status.html +++ b/templates/status.html @@ -86,7 +86,7 @@ - +