from owrx.config.core import CoreConfig from owrx.config import Config from owrx.version import openwebrx_version from owrx.map import Map, Location from owrx.aprs import getSymbolData from owrx.eibi import EIBI from owrx.repeaters import Repeaters from json import JSONEncoder from datetime import datetime, timedelta, timezone import urllib import threading import logging import json import re import os import time logger = logging.getLogger(__name__) class MyJSONEncoder(JSONEncoder): def default(self, obj): return obj.toJSON() class MarkerLocation(Location): def __init__(self, attrs): self.attrs = attrs # Making sure older cached files load self.attrs["type"] = "latlon" def getId(self): return self.attrs["id"] def getMode(self): return self.attrs["mode"] def __dict__(self): return self.attrs def toJSON(self): return self.attrs class Markers(object): sharedInstance = None creationLock = threading.Lock() @staticmethod def getSharedInstance(): with Markers.creationLock: if Markers.sharedInstance is None: Markers.sharedInstance = Markers() return Markers.sharedInstance @staticmethod def start(): Markers.getSharedInstance().startThread() @staticmethod def stop(): Markers.getSharedInstance().stopThread() @staticmethod def _getCachedMarkersFile(): coreConfig = CoreConfig() return "{data_directory}/markers.json".format(data_directory=coreConfig.get_data_directory()) def __init__(self): self.refreshPeriod = 60*60*24 self.event = threading.Event() self.fmarkers = {} self.wmarkers = {} self.smarkers = {} self.thread = None # Known database files self.fileList = [ "markers.json", "/etc/openwebrx/markers.json", ] # Find additional marker files in the markers.d folder try: markersDir = "/etc/openwebrx/markers.d" self.fileList += [ markersDir + "/" + file for file in os.listdir(markersDir) if file.endswith(".json") ] except Exception: pass # Start the main thread def startThread(self): if self.thread is None: self.event.clear() self.thread = threading.Thread(target=self._refreshThread) self.thread.start() # Stop the main thread def stopThread(self): if self.thread is not None: logger.info("Stopping marker database thread.") self.event.set() self.thread.join() # This is the actual thread function def _refreshThread(self): logger.info("Starting marker database thread...") # No markers yet self.markers = {} self.rxmarkers = {} self.txmarkers = {} self.remarkers = {} # Load miscellaneous markers from local files for file in self.fileList: if os.path.isfile(file): self.markers.update(self.loadMarkers(file)) # This file contains cached receivers database file = self._getCachedMarkersFile() ts = os.path.getmtime(file) if os.path.isfile(file) else 0 # If cached receivers database stale, update it if time.time() - ts >= self.refreshPeriod: self.rxmarkers = self.updateCache() ts = os.path.getmtime(file) if os.path.isfile(file) else 0 # If receivers database update did not run or failed, use cache if not self.rxmarkers: self.rxmarkers = self.loadMarkers(file) # Load current schedule from the EIBI database self.txmarkers = self.loadCurrentTransmitters() # Load repeaters from the Repeaters database self.remarkers = self.loadRepeaters() # Update map with markers logger.info("Updating map...") self.updateMap(self.markers) self.updateMap(self.rxmarkers) self.updateMap(self.txmarkers) self.updateMap(self.remarkers) # # Main Loop # while not self.event.is_set(): # Wait for the head of the next hour self.event.wait((60 - datetime.utcnow().minute) * 60) if self.event.is_set(): break # Load new transmitters schedule from the EIBI logger.info("Refreshing transmitters schedule..") tx = self.loadCurrentTransmitters() # Check if we need to exit if self.event.is_set(): break # Remove station markers that have no transmissions map = Map.getSharedInstance() notx = [x for x in self.txmarkers.keys() if x not in tx] for key in notx: map.removeLocation(key) del self.txmarkers[key] # Create a timestamp far into the future, for permanent markers permanent = datetime.now(timezone.utc) + timedelta(weeks=500) # Update station markers that have transmissions for key in tx.keys(): r = tx[key] map.updateLocation(r.getId(), r, r.getMode(), timestamp=permanent) self.txmarkers[key] = r # Done with the schedule notx = None tx = None # Check if we need to exit if self.event.is_set(): break # Update cached receivers data if time.time() - ts >= self.refreshPeriod: logger.info("Refreshing receivers database...") rx = self.updateCache() ts = os.path.getmtime(file) if rx: # Remove receiver markers that no longer exist norx = [x for x in self.rxmarkers.keys() if x not in rx] for key in norx: map.removeLocation(key) del self.rxmarkers[key] # Update receiver markers that are online for key in rx.keys(): r = rx[key] map.updateLocation(r.getId(), r, r.getMode(), timestamp=permanent) self.rxmarkers[key] = r # Done updating receivers norx = None rx = None # Done with the thread logger.info("Stopped marker database thread.") self.thread = None # Save markers to a given file def saveMarkers(self, file: str, markers): logger.info("Saving {0} markers to '{1}'...".format(len(markers), file)) try: with open(file, "w") as f: json.dump(markers, f, cls=MyJSONEncoder, indent=2) f.close() except Exception as e: logger.error("saveMarkers() exception: {0}".format(e)) # Load markers from a given file def loadMarkers(self, file: str): logger.info("Loading markers from '{0}'...".format(file)) # Load markers list from JSON file try: with open(file, "r") as f: db = json.load(f) f.close() except Exception as e: logger.error("loadMarkers() exception: {0}".format(e)) return # Process markers list result = {} for key in db.keys(): attrs = db[key] result[key] = MarkerLocation(attrs) # Done logger.info("Loaded {0} markers from '{1}'.".format(len(result), file)) return result # Update given markers on the map def updateMap(self, markers): # Must have valid markers to update if markers is not None: # Create a timestamp far into the future, for permanent markers permanent = datetime.now(timezone.utc) + timedelta(weeks=500) for r in markers.values(): Map.getSharedInstance().updateLocation(r.getId(), r, r.getMode(), timestamp=permanent) # Scrape online databases, updating cache file def updateCache(self): # Scrape websites for data file = self._getCachedMarkersFile() cache = {} logger.info("Scraping KiwiSDR website...") cache.update(self.scrapeKiwiSDR()) logger.info("Scraping WebSDR website...") cache.update(self.scrapeWebSDR()) logger.info("Scraping OpenWebRX website...") cache.update(self.scrapeOWRX()) # Save parsed data into a file, if there is anything to save if cache: self.saveMarkers(file, cache) # Done return cache # # Following functions scrape data from websites and internal databases # def loadRepeaters(self, rangeKm: int = 200): # No result yet result = {} # Refresh / load repeaters database, as needed Repeaters.getSharedInstance().refresh() # Load repeater sites from repeaters database for entry in Repeaters.getSharedInstance().getAllInRange(rangeKm): rl = MarkerLocation({ "type" : "latlon", "mode" : "Repeaters", "id" : entry["name"], "lat" : entry["lat"], "lon" : entry["lon"], "freq" : entry["freq"], "mmode" : entry["mode"], "status" : entry["status"], "updated" : entry["updated"], "comment" : entry["comment"] }) result[rl.getId()] = rl # Done return result def loadCurrentTransmitters(self): #url = "https://www.short-wave.info/index.php?txsite=" url = "https://www.google.com/search?q=" result = {} # Refresh / load EIBI database, as needed EIBI.getSharedInstance().refresh() # Load transmitter sites from EIBI database for entry in EIBI.getSharedInstance().currentTransmitters().values(): # Extract target regions and languages, removing duplicates schedule = entry["schedule"] langs = {} targets = {} comment = "" langstr = "" # for row in schedule: # lang = row["lang"] # target = row["tgt"] # if target and target not in targets: # targets[target] = True # comment += (", " if comment else " to ") + target # if lang and lang not in langs: # langs[lang] = True # langstr += (", " if langstr else "") + re.sub(r"(:|\s*\().*$", "", lang) # Compose comment comment = ("Transmitting" + comment) if comment else "Transmitter" comment = (comment + " (" + langstr + ")") if langstr else comment rl = MarkerLocation({ "type" : "latlon", "mode" : "Stations", "comment" : comment, "id" : entry["name"], "lat" : entry["lat"], "lon" : entry["lon"], "ttl" : entry["ttl"] * 1000, "url" : url + urllib.parse.quote_plus(entry["name"]), "schedule": schedule }) result[rl.getId()] = rl # Done logger.info("Loaded {0} transmitters from EIBI.".format(len(result))) return result def scrapeOWRX(self, url: str = "https://www.receiverbook.de/map"): patternJson = re.compile(r"^\s*var\s+receivers\s+=\s+(\[.*\]);\s*$") result = {} try: data = None for line in urllib.request.urlopen(url).readlines(): # Convert read bytes to a string line = line.decode('utf-8') # When we encounter a URL... m = patternJson.match(line) if m: data = json.loads(m.group(1)) break if data is not None: for entry in data: lat = entry["location"]["coordinates"][1] lon = entry["location"]["coordinates"][0] for r in entry["receivers"]: if "version" in r: dev = r["type"] + " " + r["version"] else: dev = r["type"] rl = MarkerLocation({ "type" : "latlon", "mode" : r["type"], "id" : re.sub(r"^.*://(.*?)(/.*)?$", r"\1", r["url"]), "lat" : lat, "lon" : lon, "comment" : r["label"], "url" : r["url"], "device" : dev }) result[rl.getId()] = rl # Offset colocated receivers by ~500m lon = lon + 0.0005 except Exception as e: logger.error("scrapeOWRX() exception: {0}".format(e)) # Done return result def scrapeWebSDR(self, url: str = "http://websdr.ewi.utwente.nl/~~websdrlistk?v=1&fmt=2&chseq=0"): result = {} try: data = urllib.request.urlopen(url).read().decode('utf-8') data = json.loads(re.sub(r"^\s*//.*", "", data, flags=re.MULTILINE)) for entry in data: if "lat" in entry and "lon" in entry and "url" in entry: # Save accumulated attributes, use hostname as key lat = entry["lat"] lon = entry["lon"] rl = MarkerLocation({ "type" : "latlon", "mode" : "WebSDR", "id" : re.sub(r"^.*://(.*?)(/.*)?$", r"\1", entry["url"]), "lat" : lat, "lon" : lon, "comment" : entry["desc"], "url" : entry["url"], "users" : int(entry["users"]), "device" : "WebSDR" }) result[rl.getId()] = rl except Exception as e: logger.error("scrapeWebSDR() exception: {0}".format(e)) # Done return result def scrapeKiwiSDR(self, url: str = "http://kiwisdr.com/public/"): result = {} try: patternAttr = re.compile(r".*.*") patternUrl = re.compile(r".*.*.*") patternGps = re.compile(r"\(\s*(-?\d+\.\d+)\s*,\s*(-?\d+\.\d+)\s*\)") entry = {} for line in urllib.request.urlopen(url).readlines(): # Convert read bytes to a string line = line.decode('utf-8') # When we encounter a URL... m = patternUrl.match(line) if m is not None: # Add URL attribute entry["url"] = m.group(1) # Must have "gps" attribut with latitude / longitude if "gps" in entry and "url" in entry: m = patternGps.match(entry["gps"]) if m is not None: # Save accumulated attributes, use hostname as key lat = float(m.group(1)) lon = float(m.group(2)) rl = MarkerLocation({ "type" : "latlon", "mode" : "KiwiSDR", "id" : re.sub(r"^.*://(.*?)(/.*)?$", r"\1", entry["url"]), "lat" : lat, "lon" : lon, "comment" : entry["name"], "url" : entry["url"], "users" : int(entry["users"]), "maxusers": int(entry["users_max"]), "loc" : entry["loc"], "altitude": int(entry["asl"]), "antenna" : entry["antenna"], "device" : re.sub("_v", " ", entry["sw_version"]) }) result[rl.getId()] = rl # Clear current entry entry = {} else: # Save all parsed attributes in the current entry m = patternAttr.match(line) if m is not None: # Save attribute in the current entry entry[m.group(1).lower()] = m.group(2) except Exception as e: logger.error("scrapeKiwiSDR() exception: {0}".format(e)) # Done return result