diff --git a/config-example.yml b/config-example.yml
index e9be2ab..3e36a65 100644
--- a/config-example.yml
+++ b/config-example.yml
@@ -249,6 +249,18 @@ sig_ref_data_providers:
- class: "FEA"
enabled: true
+ - class: "DMVE"
+ enabled: true
+
+ - class: "DMUE"
+ enabled: true
+
+ - class: "DCE"
+ enabled: true
+
+ - class: "DEFE"
+ enabled: true
+
- class: "KRMNPA"
enabled: true
diff --git a/core/constants.py b/core/constants.py
index f33d3bc..31cabe7 100644
--- a/core/constants.py
+++ b/core/constants.py
@@ -247,13 +247,53 @@ SIGS = [
region_flag="🇪🇸",
refs_globally_unique=True,
),
+ SIG(
+ name="DMUE",
+ comment_names=["DMUE"],
+ description="Diploma Museos de España",
+ sig_type=SIGType.REGIONAL,
+ ref_regex=r"MUE[A-Z]{2}-\d{3}",
+ icon="fa-landmark",
+ region_flag="🇪🇸",
+ refs_globally_unique=True,
+ ),
+ SIG(
+ name="DMVE",
+ comment_names=["DMVE"],
+ description="Diploma Monumentos y Vestigios de España",
+ sig_type=SIGType.REGIONAL,
+ ref_regex=r"MV[A-Z]{1,2}-\d{4}",
+ icon="fa-monument",
+ region_flag="🇪🇸",
+ refs_globally_unique=True,
+ ),
+ SIG(
+ name="DCE",
+ comment_names=["DCE"],
+ description="Diploma Castillos de España",
+ sig_type=SIGType.REGIONAL,
+ ref_regex=r"C[A-Z]{1,2}-\d{3}",
+ icon="fa-chess-rook",
+ region_flag="🇪🇸",
+ refs_globally_unique=False,
+ ),
+ SIG(
+ name="DEFE",
+ comment_names=["DEFE"],
+ description="Diploma Estaciones de Ferrocarril de España",
+ sig_type=SIGType.REGIONAL,
+ ref_regex=r"EF[A-Z]{1,2}-\d{3}",
+ icon="fa-train",
+ region_flag="🇪🇸",
+ refs_globally_unique=True,
+ ),
SIG(
name="DTMBA",
comment_names=["DTMBA"],
description="Diploma Teatri Musei e Belle Arti",
sig_type=SIGType.REGIONAL,
ref_regex=r"I-?[0-9]{3,4}\s?[A-Z]{2}",
- icon="fa-masks-theater",
+ icon="fa-landmark",
region_flag="🇮🇹",
refs_globally_unique=True,
),
diff --git a/providers/sigrefdata/dce.py b/providers/sigrefdata/dce.py
new file mode 100644
index 0000000..8046687
--- /dev/null
+++ b/providers/sigrefdata/dce.py
@@ -0,0 +1,42 @@
+import io
+from time import sleep
+
+import pandas as pd
+
+from core.enums import SIGRefType
+from data.sig_ref import SIGRef
+from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
+
+
+class DCE(FileDownloadSIGRefDataProvider):
+ """SIG ref data provider for Diploma Castillos de España"""
+
+ POLL_INTERVAL_DAYS = 365
+ SIG = "DCE"
+ DATA_URL = "https://www.acracb.org/dce/descargas/General/directorio_referencias_dce.xls"
+
+ 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 = []
+
+ file_stream = io.BytesIO(http_response.content)
+ df = pd.read_excel(file_stream, engine="xlrd", header=None)
+
+ for index, row in df.iterrows():
+ if row.iloc[0] and row.iloc[2]:
+ new_data.append(
+ SIGRef(sig=self.SIG, id=row.iloc[0].strip(), name=row.iloc[2].strip(), ref_type=SIGRefType.CASTLE)
+ )
+
+ # 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/providers/sigrefdata/defe.py b/providers/sigrefdata/defe.py
new file mode 100644
index 0000000..afeb05f
--- /dev/null
+++ b/providers/sigrefdata/defe.py
@@ -0,0 +1,46 @@
+import io
+from time import sleep
+
+import pandas as pd
+
+from core.enums import SIGRefType
+from data.sig_ref import SIGRef
+from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
+
+
+class DEFE(FileDownloadSIGRefDataProvider):
+ """SIG ref data provider for Diploma Estationes de Ferrocarril de España"""
+
+ POLL_INTERVAL_DAYS = 365
+ SIG = "DEFE"
+ DATA_URL = "https://www.acracb.org/defe/descargas/General/directorio_referencias_defe.xls"
+
+ 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 = []
+
+ file_stream = io.BytesIO(http_response.content)
+ df = pd.read_excel(file_stream, engine="xlrd", header=None)
+
+ for index, row in df.iterrows():
+ # Skip the header row
+ if str(row.iloc[0]) == "NºDEFE":
+ continue
+
+ if row.iloc[0] and row.iloc[1]:
+ new_data.append(
+ SIGRef(sig=self.SIG, id=row.iloc[0].strip(), name=row.iloc[1].strip(), ref_type=SIGRefType.BUILDING)
+ )
+
+ # 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/providers/sigrefdata/dmue.py b/providers/sigrefdata/dmue.py
new file mode 100644
index 0000000..2acf4f7
--- /dev/null
+++ b/providers/sigrefdata/dmue.py
@@ -0,0 +1,37 @@
+import csv
+from time import sleep
+
+from core.enums import SIGRefType
+from data.sig_ref import SIGRef
+from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
+
+
+class DMUE(FileDownloadSIGRefDataProvider):
+ """SIG ref data provider for Diploma Museos de España"""
+
+ POLL_INTERVAL_DAYS = 365
+ SIG = "DMUE"
+ DATA_URL = "https://dmue.radiogalena.es/nom_dmue.csv"
+
+ 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 = []
+
+ for row in csv.reader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
+ if len(row) > 1 and row[0] and row[1]:
+ new_data.append(
+ SIGRef(sig=self.SIG, id=row[0].strip(), name=row[1].strip(), ref_type=SIGRefType.BUILDING)
+ )
+
+ # 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/providers/sigrefdata/dmve.py b/providers/sigrefdata/dmve.py
new file mode 100644
index 0000000..c92fbf3
--- /dev/null
+++ b/providers/sigrefdata/dmve.py
@@ -0,0 +1,50 @@
+import io
+from time import sleep
+
+import pandas as pd
+
+from core.enums import SIGRefType
+from data.sig_ref import SIGRef
+from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
+
+
+class DMVE(FileDownloadSIGRefDataProvider):
+ """SIG ref data provider for Diploma Monumentos y Vestigios de España"""
+
+ POLL_INTERVAL_DAYS = 365
+ SIG = "DMVE"
+ DATA_URL = "https://www.acracb.org/dmve/descargas/General/directorio_referencias_dmve.xls"
+
+ 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 = []
+
+ file_stream = io.BytesIO(http_response.content)
+ # Despide the .xls extension this is actually an xlsx file, so we need openpyxl not xlrd
+ df = pd.read_excel(file_stream, engine="openpyxl", header=None)
+
+ for index, row in df.iterrows():
+ ref = row.iloc[0]
+ name = row.iloc[1]
+
+ # Skip the header row and blank rows
+ if str(ref) == "REF.":
+ continue
+ if pd.isna(ref) or pd.isna(name):
+ continue
+
+ if ref and name:
+ new_data.append(SIGRef(sig=self.SIG, id=ref.strip(), name=name.strip(), ref_type=SIGRefType.BUILDING))
+
+ # 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/requirements.txt b/requirements.txt
index 6a89668..501cff6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,9 +16,12 @@ beautifulsoup4~=4.14.2
websocket-client~=1.8.0
tornado~=6.4.2
tornado_eventsource~=3.0.0
+pandas~=3.0.0
geopandas~=0.13.2
simplejson~=4.1.1
cachetools~=7.1.6
fastkml~=1.4.0
ruff~=0.16.3
-pdfplumber~=0.11.10
\ No newline at end of file
+pdfplumber~=0.11.10
+xlrd~=2.0.2
+openpyxl~=3.1.5
\ No newline at end of file
diff --git a/templates/about.html b/templates/about.html
index 4af1230..5545578 100644
--- a/templates/about.html
+++ b/templates/about.html
@@ -102,9 +102,11 @@
on the Air (SIOTA), World Castles Award (WCA), New Zealand on the Air (ZLOTA), Keith Roget Memorial National
Parks Award (KRMNPA), South Australia National Parks and Conservation Parks Award (SANPCPA), Wainwrights on the
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), Castles on the Air (COTA), Polish Gmina Award (PGA), and Toilets on the Air.
+ the Air, Worked All Britain (WAB), Worked All Ireland (WAI), Diploma Municipios de España (DME), Diploma
+ Faros de España (FEA), Diploma Muesos de España (DMUE), Diploma Castillos de España (DCE), Diploma Monumentos y
+ Vestigios de España (DMVE), Diploma Estaciones de Ferrocarril de España (DEFE), Diploma Teatri Musei e Belle
+ Arti (DTMBA), British Inland Waterways 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!