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 **")
예제 #2
0
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)
예제 #3
0
    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
예제 #4
0
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)
예제 #5
0
    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
예제 #6
0
    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
예제 #7
0
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
예제 #8
0
    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
예제 #9
0
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,
예제 #10
0
    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
예제 #11
0
    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