def __init__(self, name: str, debug: bool = False): self.logger = logging.getLogger(name) logpath: str = resolve_external_file_path("/logs") if not os.path.isdir(logpath): try: # Try creating the directory. os.mkdir(logpath) except Exception: print("Failed to create log directory.") return filename: str = resolve_external_file_path("/logs/" + name + ".log") if not os.path.isfile(filename): try: # Try creating the file. file = open(filename, "x") file.close() except Exception: print("Failed to create log file.") return self.logger.setLevel(logging.DEBUG if debug else logging.INFO) fh = RotatingFileHandler( filename, maxBytes=LOG_MAX_SIZE_MB * (1024**2), backupCount=LOG_BACKUP_COUNT, ) formatter = logging.Formatter( "%(asctime)s | %(levelname)s | %(message)s") fh.setFormatter(formatter) # add the handler to the logger self.logger.addHandler(fh) self.logger.info("** LOGGER STARTED **")
def ui_logs_render(request, path): page = request.args.get("page") if not page: return redirect(f"/logs/{path}?page=1") page = int(page) assert page >= 1 try: log_file = open( resolve_external_file_path("/logs/{}.log").format(path)) except FileNotFoundError: abort(404) return data = { "logs": log_file.read().splitlines()[-300 * page:( -300 * (page - 1) if page > 1 else None)][::-1], "ui_page": "logs", "ui_title": "Logs - {}".format(path), "page": page, } log_file.close() return render_template("log.html", data=data)
def __init__(self): # Player count only changes after server restart, may as well just load this once. with open(resolve_external_file_path("state/BAPSicleServer.json")) as file: self._server_state = json.loads(file.read()) self._player_count = int(self._server_state["num_channels"]) self._states = [None] * self._player_count
def ui_logs_list(request): files = os.listdir(resolve_external_file_path("/logs")) log_files = [] for file_name in files: if file_name.endswith(".log"): log_files.append(file_name.rstrip(".log")) log_files.sort() data = {"ui_page": "logs", "ui_title": "Logs", "logs": log_files} return render_template("loglist.html", data=data)
def get_alerts(self): with open(resolve_external_file_path("state/BAPSicleServer.json")) as file: self._state = json.loads(file.read()) funcs = [self._api_key, self._start_time] alerts: List[Alert] = [] for func in funcs: func_alerts = func() if func_alerts: alerts.extend(func_alerts) return alerts
def get_alerts(self): for channel in range(self._player_count): with open(resolve_external_file_path("state/Player{}.json".format(channel))) as file: self._states[channel] = json.loads(file.read()) funcs = [self._channel_count, self._initialised, self._start_time] alerts: List[Alert] = [] for func in funcs: func_alerts = func() if func_alerts: alerts.extend(func_alerts) return alerts
async def audio_file(request, type: str, id: int): if type not in ["managed", "track"]: abort(404) filename = resolve_external_file_path("music-tmp/{}-{}.mp3".format( type, id)) # Swap with a normalised version if it's ready, else returns original. filename = get_normalised_filename_if_available(filename) # Send file or 404 try: response = await file(filename) except FileNotFoundError: abort(404) return return response
def __init__(self, channel_from_q: List[Queue], server_config: StateManager): self.logger = LoggingManager("FileManager") self.api = MyRadioAPI(self.logger, server_config) process_title = "File Manager" setproctitle(process_title) current_process().name = process_title terminator = Terminator() self.normalisation_mode = server_config.get()["normalisation_mode"] if self.normalisation_mode != "on": self.logger.log.info("Normalisation is disabled.") else: self.logger.log.info("Normalisation is enabled.") self.channel_count = len(channel_from_q) self.channel_received = None self.last_known_show_plan = [[]] * self.channel_count self.next_channel_preload = 0 self.known_channels_preloaded = [False] * self.channel_count self.known_channels_normalised = [False] * self.channel_count self.last_known_item_ids = [[]] * self.channel_count try: while not terminator.terminate: # If all channels have received the delete command, reset for the next one. if ( self.channel_received is None or self.channel_received == [True] * self.channel_count ): self.channel_received = [False] * self.channel_count for channel in range(self.channel_count): try: message = channel_from_q[channel].get_nowait() except Exception: continue try: # source = message.split(":")[0] command = message.split(":", 2)[1] # If we have requested a new show plan, empty the music-tmp directory for the previous show. if command == "GETPLAN": if ( self.channel_received != [ False] * self.channel_count and self.channel_received[channel] is False ): # We've already received a delete trigger on a channel, # let's not delete the folder more than once. # If the channel was already in the process of being deleted, the user has # requested it again, so allow it. self.channel_received[channel] = True continue # Delete the previous show files! # Note: The players load into RAM. If something is playing over the load, # the source file can still be deleted. path: str = resolve_external_file_path( "/music-tmp/") if not os.path.isdir(path): self.logger.log.warning( "Music-tmp folder is missing, not handling." ) continue files = [ f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) ] for file in files: if isWindows(): filepath = path + "\\" + file else: filepath = path + "/" + file self.logger.log.info( "Removing file {} on new show load.".format( filepath ) ) try: os.remove(filepath) except Exception: self.logger.log.warning( "Failed to remove, skipping. Likely file is still in use." ) continue self.channel_received[channel] = True self.known_channels_preloaded = [ False] * self.channel_count self.known_channels_normalised = [ False ] * self.channel_count # If we receive a new status message, let's check for files which have not been pre-loaded. if command == "STATUS": extra = message.split(":", 3) if extra[2] != "OKAY": continue status = json.loads(extra[3]) show_plan = status["show_plan"] item_ids = [] for item in show_plan: item_ids += item["timeslotitemid"] # If the new status update has a different order / list of items, # let's update the show plan we know about # This will trigger the chunk below to do the rounds again and preload any new files. if item_ids != self.last_known_item_ids[channel]: self.last_known_item_ids[channel] = item_ids self.last_known_show_plan[channel] = show_plan self.known_channels_preloaded[channel] = False except Exception: self.logger.log.exception( "Failed to handle message {} on channel {}.".format( message, channel ) ) # Let's try preload / normalise some files now we're free of messages. preloaded = self.do_preload() normalised = self.do_normalise() if not preloaded and not normalised: # We didn't do any hard work, let's sleep. sleep(0.2) except Exception as e: self.logger.log.exception( "Received unexpected exception: {}".format(e)) del self.logger
from helpers.logging_manager import LoggingManager from helpers.device_manager import DeviceManager from helpers.state_manager import StateManager from helpers.the_terminator import Terminator from helpers.normalisation import get_normalised_filename_if_available from helpers.myradio_api import MyRadioAPI from helpers.alert_manager import AlertManager import package from baps_types.happytime import happytime env = Environment( loader=FileSystemLoader("%s/ui-templates/" % os.path.dirname(__file__)), autoescape=select_autoescape(), ) LOG_FILEPATH = resolve_external_file_path("logs") LOG_FILENAME = LOG_FILEPATH + "/WebServer.log" # From Sanic's default, but set to log to file. os.makedirs(LOG_FILEPATH, exist_ok=True) LOGGING_CONFIG = dict( version=1, disable_existing_loggers=False, loggers={ "sanic.root": { "level": "INFO", "handlers": ["file"] }, "sanic.error": { "level": "INFO", "handlers": ["error_file"], "propagate": True,
def __init__( self, name, logger: LoggingManager, default_state: Dict[str, Any] = None, rate_limit_params=[], rate_limit_period_s=5, ): self.logger = logger path_dir: str = resolve_external_file_path("/state") if not os.path.isdir(path_dir): try: # Try creating the directory. os.mkdir(path_dir) except Exception: self._logException("Failed to create state directory.") return self.filepath = resolve_external_file_path("/state/" + name + ".json") self._log("State file path set to: " + self.filepath) if not os.path.isfile(self.filepath): self._log("No existing state file found.") try: # Try creating the file. open(self.filepath, "x") except Exception: self._logException("Failed to create state file.") return file_raw: str with open(self.filepath, "r") as file: file_raw = file.read() if file_raw == "": self._log("State file is empty. Setting default state.") self.state = default_state else: try: file_state: Dict[str, Any] = json.loads(file_raw) # Turn from JSON -> PlanItem if "channel" in file_state: file_state["loaded_item"] = ( PlanItem(file_state["loaded_item"]) if file_state["loaded_item"] else None ) file_state["show_plan"] = [ PlanItem(obj) for obj in file_state["show_plan"] ] # Now feed the loaded state into the initialised state manager. self.state = file_state # If there are any new config options in the default state, save them. # Uses update() to save them to file too. if default_state: for key in default_state.keys(): if key not in file_state.keys(): self.update(key, default_state[key]) except Exception: self._logException( "Failed to parse state JSON. Resetting to default state." ) self.state = default_state # Now setup the rate limiting # Essentially rate limit all values to "now" to start with, allowing the first update # of all vars to succeed. for param in rate_limit_params: self.__rate_limit_params_until[param] = self._currentTimeS self.__rate_limit_period_s = rate_limit_period_s
async def get_filename(self, item: PlanItem, did_download: bool = False, redownload=False): format = "mp3" # TODO: Maybe we want this customisable? if item.trackid: itemType = "track" id = item.trackid url = "/NIPSWeb/secure_play?trackid={}&{}".format(id, format) elif item.managedid: itemType = "managed" id = item.managedid url = "/NIPSWeb/managed_play?managedid={}".format(id) else: return (None, False) if did_download else None # Now check if the file already exists path: str = resolve_external_file_path("/music-tmp/") dl_suffix = ".downloading" if not os.path.isdir(path): self._log("Music-tmp folder is missing, attempting to create.") try: os.mkdir(path) except Exception as e: self._logException("Failed to create music-tmp folder: {}".format(e)) return (None, False) if did_download else None filename: str = resolve_external_file_path( "/music-tmp/{}-{}.{}".format(itemType, id, format) ) if not redownload: # Check if we already downloaded the file. If we did, give that, unless we're forcing a redownload. if os.path.isfile(filename): self._log("Already got file: " + filename, DEBUG) return (filename, False) if did_download else filename # If something else (another channel, the preloader etc) is downloading the track, wait for it. if os.path.isfile(filename + dl_suffix): time_waiting_s = 0 self._log( "Waiting for download to complete from another worker. " + filename, DEBUG, ) while time_waiting_s < 20: # TODO: Make something better here. # If the connectivity is super poor or we're loading reeaaaalllly long files, # this may be annoying, but this is just in case somehow the other api download gives up. if os.path.isfile(filename): # Now the file is downloaded successfully return (filename, False) if did_download else filename time_waiting_s += 1 self._log("Still waiting", DEBUG) time.sleep(1) # File doesn't exist, download it. try: # Just create the file to stop other sources from trying to download too. open(filename + dl_suffix, "a").close() except Exception: self.logger.log.exception("Couldn't create new temp file.") return (None, False) if did_download else None request = await self.async_api_call(url, api_version="non") if not request or not isinstance(request, (bytes, bytearray)): # Remove the .downloading temp file given we gave up trying to download. os.remove(filename + dl_suffix) return (None, False) if did_download else None try: with open(filename + dl_suffix, "wb") as file: file.write(request) os.rename(filename + dl_suffix, filename) except Exception as e: self._logException("Failed to write music file: {}".format(e)) return (None, False) if did_download else None self._log("Successfully re/downloaded file.", DEBUG) return (filename, True) if did_download else filename