Пример #1
0
    def do_POST(self):
        ctype = self.headers.get("content-type")
        if ctype != "application/x-www-form-urlencoded":
            self.error(400, "Play service received a bad request.")
            return

        length = int(self.headers.get("content-length"))
        postvars = urllib.parse.parse_qs(self.rfile.read(length),
                                         keep_blank_values=1)

        method = self.path.lstrip("/").split("/")[0]
        params = {}
        for key in postvars:
            params[key.decode("utf-8")] = postvars[key][0].decode("utf-8")

        if method not in self.server.methods:
            self.error(501)
            return

        try:
            result = self.server.methods[method](**params)
            if result.is_error:
                logging.error(result.message)
            elif result["message"]:
                logging.info(result.message)
            self.result(result.dict)
        except Exception:
            msg = log_traceback()
            self.result(NebulaResponse(500, msg).dict)
Пример #2
0
def create_ft_index(meta):
    ft = {}
    if "subclips" in meta:
        weight = 8
        for sc in [k.get("title", "") for k in meta["subclips"]]:
            try:
                for word in slugify(sc, make_set=True, min_length=3):
                    if word not in ft:
                        ft[word] = weight
                    else:
                        ft[word] = max(ft[word], weight)
            except Exception:
                logging.error("Unable to slugify subclips data")
    for key in meta:
        if key not in meta_types:
            continue
        weight = meta_types[key]["fulltext"]
        if not weight:
            continue
        try:
            for word in slugify(meta[key], make_set=True, min_length=3):
                if word not in ft:
                    ft[word] = weight
                else:
                    ft[word] = max(ft[word], weight)
        except Exception:
            logging.error(
                f"Unable to slugify key {key} with value {meta[key]}")
    return ft
Пример #3
0
 def fail(self, message="Failed", critical=False):
     if critical:
         retries = MAX_RETRIES
     else:
         retries = self.retries + 1
     self.db.query(
         """
         UPDATE jobs SET
             id_service=NULL,
             retries=%s,
             priority=%s,
             status=3,
             progress=0,
             message=%s
         WHERE id=%s
         """,
         [retries, max(0, self.priority - 1), message, self.id],
     )
     self.db.commit()
     self.status = JobState.FAILED
     logging.error(f"{self}: {message}")
     messaging.send(
         "job_progress",
         id=self.id,
         id_asset=self.id_asset,
         id_action=self.id_action,
         status=JobState.FAILED,
         progress=0,
         message=message,
     )
Пример #4
0
    def channel_recover(self):
        logging.warning("Performing recovery")

        db = DB()
        db.query(
            """
            SELECT id_item, start FROM asrun
            WHERE id_channel = %s ORDER BY id DESC LIMIT 1
            """,
            [self.id_channel],
        )
        try:
            last_id_item, last_start = db.fetchall()[0]
        except IndexError:
            logging.error("Unable to perform recovery.")
        last_item = Item(last_id_item, db=db)
        last_item.asset

        self.controller.current_item = last_item
        self.controller.cued_item = False
        self.controller.cued_fname = False

        if last_start + last_item.duration <= time.time():
            logging.info(f"Last {last_item} has been broadcasted.")
            new_item = self.cue_next(item=last_item, db=db, play=True)
        else:
            logging.info(f"Last {last_item} has not been fully broadcasted.")
            new_item = self.cue_next(item=last_item, db=db)

        if not new_item:
            logging.error("Recovery failed. Unable to cue")
            return

        self.on_change()
Пример #5
0
 def on_edit_item(self):
     objs = [
         obj for obj in self.selected_objects if obj.object_type == "item"
         and obj["item_role"] in ["live", "placeholder"]
     ]
     if not objs:
         return
     obj = objs[0]
     dlg = PlaceholderDialog(self, obj.meta)
     dlg.exec_()
     if not dlg.ok:
         return False
     data = {}
     for key in dlg.meta:
         if dlg.meta[key] != obj[key]:
             data[key] = dlg.meta[key]
     if not data:
         return
     response = api.set(object_type=obj.object_type,
                        objects=[obj.id],
                        data=data)
     if not response:
         logging.error(response.message)
         return
     self.model().load()
Пример #6
0
    def login(self, *args, **kwargs):
        if args:
            return self.render_error(400, "Bad request")
        if cherrypy.request.method != "POST":
            return self.render_error(400, "Bad request")
        request = parse_request(**kwargs)
        login = request.get("login", "-")
        password = request.get("password", "-")
        user_data = self.parent["login_helper"](login, password)

        if not user_data:
            logging.error(f"Incorrect login ({login})")
            if kwargs.get("api", False):
                return json_response(
                    401, "Invalid user name / password combination")
            return self.default(error="Invalid login/password combination")

        if "password" in user_data:
            del (user_data["password"])

        client_info = get_client_info()

        logging.goodnews("User {} logged in".format(login))
        session_id = self.sessions.create(user_data, **client_info)
        save_session_cookie(self, session_id)

        if request.get("api", False):
            return json_response(200, data=user_data, session_id=session_id)
        raise cherrypy.HTTPRedirect(request.get("from_page", "/"))
Пример #7
0
def cg_download(target_path, method, timeout=10, verbose=True, **kwargs):
    start_time = time.monotonic()
    target_dir = os.path.dirname(os.path.abspath(target_path))
    cg_server = config.get("cg_server", "https://cg.immstudios.org")
    cg_site = config.get("cg_site", config["site_name"])
    if not os.path.isdir(target_dir):
        try:
            os.makedirs(target_dir)
        except Exception:
            logging.error(f"Unable to create output directory {target_dir}")
            return False
    url = f"{cg_server}/render/{cg_site}/{method}"
    try:
        response = requests.get(url, params=kwargs, timeout=timeout)
    except Exception:
        log_traceback("Unable to download CG item")
        return False
    if response.status_code != 200:
        logging.error(f"CG Download failed with code {response.status_code}")
        return False
    try:
        temp_path = target_path + ".creating"
        with open(temp_path, "wb") as f:
            f.write(response.content)
        os.rename(temp_path, target_path)
    except Exception:
        log_traceback(f"Unable to write CG item to {target_path}")
        return False
    if verbose:
        elapsed = time.monotonic() - start_time
        logging.info(f"CG {method} downloaded in {elapsed:.02f}s")
    return True
Пример #8
0
 def __call__(self, message):
     tstamp = int(message.timestamp * 1000000000)
     tags = {
         "user": message.data["user"],
         "site_name": message.site_name,
         "level": {
             0: "debug",
             1: "info",
             2: "warning",
             3: "error",
             4: "info"
         }[message.data["message_type"]],
     }
     data = {
         "streams": [{
             "stream": tags,
             "values": [[f"{tstamp}", message.data["message"]]]
         }]
     }
     try:
         self.session.post(
             self.url,
             data=json.dumps(data),
             headers={"Content-Type": "application/json"},
             timeout=0.2,
         )
     except Exception:
         logging.error("Unable to send log message to Loki")
Пример #9
0
    def mount(self, storage):
        if storage["protocol"] == "samba":
            smbopts = {}
            if storage.get("login"):
                smbopts["user"] = storage["login"]
            if storage.get("password"):
                smbopts["pass"] = storage["password"]
            if storage.get("domain"):
                smbopts["domain"] = storage["domain"]
            smbver = storage.get("samba_version", "3.0")
            if smbver:
                smbopts["vers"] = smbver

            if smbopts:
                opts = " -o '{}'".format(
                    ",".join(["{}={}".format(k, smbopts[k]) for k in smbopts])
                )
            else:
                opts = ""
            cmd = f"mount.cifs {storage['path']} {storage.local_path}{opts}"

        elif storage["protocol"] == "nfs":
            cmd = f"mount.nfs {storage['path']} {storage.local_path}"
        else:
            return

        proc = subprocess.Popen(cmd, shell=True)
        while proc.poll() is None:
            time.sleep(0.1)
        if proc.returncode:
            logging.error(f"Unable to mount {storage}")
Пример #10
0
def get_solver(solver_name):
    if not get_plugin_path("solver"):
        return

    for f in [
        FileObject(get_plugin_path("solver"), solver_name + ".py"),
        FileObject(get_plugin_path("solver"), solver_name, solver_name + ".py"),
    ]:

        if f.exists:
            sys.path.insert(0, f.dir_name)
            try:
                py_mod = imp.load_source(solver_name, f.path)
                break
            except Exception:
                log_traceback("Unable to load plugin {}".format(solver_name))
                return
    else:
        logging.error("{} does not exist".format(f))
        return

    if "Plugin" not in dir(py_mod):
        logging.error("No plugin class found in {}".format(f))
        return
    return py_mod.Plugin
Пример #11
0
    def load(self):
        self.plugins = []
        bpath = get_plugin_path("playout")
        if not bpath:
            logging.warning("Playout plugins directory does not exist")
            return

        for plugin_name in self.service.channel_config.get("plugins", []):
            plugin_file = plugin_name + ".py"
            plugin_path = os.path.join(bpath, plugin_file)

            if not os.path.exists(plugin_path):
                logging.error(f"Plugin {plugin_name} does not exist")
                continue

            try:
                py_mod = imp.load_source(plugin_name, plugin_path)
            except Exception:
                log_traceback(f"Unable to load plugin {plugin_name}")
                continue

            if "Plugin" not in dir(py_mod):
                logging.error(f"No plugin class found in {plugin_file}")
                continue

            logging.info("Initializing plugin {}".format(plugin_name))
            self.plugins.append(py_mod.Plugin(self.service))
            self.plugins[
                -1].title = self.plugins[-1].title or plugin_name.capitalize()
        logging.info("All plugins initialized")
Пример #12
0
    def load_callback(self, callback, response):
        self.beginResetModel()
        QApplication.setOverrideCursor(Qt.WaitCursor)

        if not response:
            logging.error(response.message)
        else:
            asset_cache.request(response.data)

        # Pagination

        current_page = self.parent().current_page

        if len(response.data) > RECORDS_PER_PAGE:
            page_count = current_page + 1
        elif len(response.data) == 0:
            page_count = max(1, current_page - 1)
        else:
            page_count = current_page

        if current_page > page_count:
            current_page = page_count

        # Replace object data

        if len(response.data) > RECORDS_PER_PAGE:
            response.data.pop(-1)
        self.object_data = [asset_cache.get(row[0]) for row in response.data]

        self.parent().set_page(current_page, page_count)
        self.endResetModel()
        QApplication.restoreOverrideCursor()

        callback()
Пример #13
0
    def send_message(self, method, **data):
        if not (self.connection and self.channel):
            if not self.connect():
                time.sleep(1)
                return

        message = json.dumps([
            time.time(),
            config["site_name"],
            config["host"],
            method,
            data
        ])

        try:
            self.channel.basic_publish(
                exchange='',
                routing_key=config["site_name"],
                body=message
            )
        except pika.exceptions.ChannelWrongStateError:
            logging.warning("RabbitMQ: nobody's listening", handlers=[])
            return
        except pika.exceptions.StreamLostError:
            logging.error("RabbitMQ connection lost", handlers=[])
            self.connection = self.channel = None
        except:
            log_traceback("RabbitMQ error", handlers=[])
            logging.debug("Unable to send message" , message, handlers=[])
            self.connection = self.channel = None
Пример #14
0
    def __init__(self, parent, objects=[]):
        super(SendToDialog, self).__init__(parent)
        self.objects = list(objects)
        self.setModal(True)

        if len(self.objects) == 1:
            what = self.objects[0]["title"]
        else:
            what = f"{len(self.objects)} objects"

        self.setWindowTitle(f"Send {what} to...")

        self.actions = []
        response = api.actions(objects=self.assets)
        if not response:
            logging.error(response.message)
            self.close()
        else:
            layout = QVBoxLayout()
            for id_action, title in response.data:
                btn_send = ActionButton(title)
                btn_send.clicked.connect(functools.partial(self.on_send, id_action))
                layout.addWidget(btn_send, 1)

            self.restart_existing = QCheckBox("Restart existing actions", self)
            self.restart_existing.setChecked(True)
            layout.addWidget(self.restart_existing, 0)

            self.restart_running = QCheckBox("Restart running actions", self)
            self.restart_running.setChecked(False)
            layout.addWidget(self.restart_running, 0)

            self.setLayout(layout)
            self.setMinimumWidth(400)
Пример #15
0
    def on_delete_event(self):
        if not self.calendar.selected_event:
            return
        cursor_event = self.calendar.selected_event
        if not has_right("scheduler_edit", self.id_channel):
            logging.error(
                "You are not allowed to modify schedule of this channel.")
            return

        ret = QMessageBox.question(
            self,
            "Delete event",
            f"Do you really want to delete {cursor_event}?"
            "\nThis operation cannot be undone.",
            QMessageBox.Yes | QMessageBox.No,
        )
        if ret == QMessageBox.Yes:
            QApplication.processEvents()
            self.calendar.setCursor(Qt.WaitCursor)
            response = api.schedule(
                id_channel=self.id_channel,
                start_time=self.calendar.week_start_time,
                end_time=self.calendar.week_end_time,
                delete=[cursor_event.id],
            )
            self.calendar.setCursor(Qt.ArrowCursor)
            if response:
                logging.info(f"{cursor_event} deleted")
                self.calendar.set_data(response.data)
            else:
                logging.error(response.message)
                self.calendar.load()
Пример #16
0
    def on_activate(self, mi):
        obj = self.model().object_data[mi.row()]
        can_mcr = has_right("mcr", self.id_channel)
        if obj.object_type == "item":

            if obj.id:
                if obj["item_role"] == "placeholder":
                    self.on_edit_item()

                elif self.parent().mcr and self.parent().mcr.isVisible(
                ) and can_mcr:
                    response = api.playout(
                        timeout=1,
                        action="cue",
                        id_channel=self.id_channel,
                        id_item=obj.id,
                    )
                    if not response:
                        logging.error(response.message)
                    self.clearSelection()

        # Event edit
        elif obj.object_type == "event" and (
                has_right("scheduler_view", self.id_channel)
                or has_right("scheduler_edit", self.id_channel)):
            self.on_edit_event()
        self.clearSelection()
Пример #17
0
    def listen_rabbit(self):
        try:
            import pika
        except ModuleNotFoundError:
            critical_error("'pika' module is not installed")

        host = config.get("rabbitmq_host", "rabbitmq")
        conparams = pika.ConnectionParameters(host=host)

        while True:
            try:
                connection = pika.BlockingConnection(conparams)
                channel = connection.channel()

                result = channel.queue_declare(
                    queue=config["site_name"],
                    arguments={"x-message-ttl": 1000})
                queue_name = result.method.queue

                logging.info("Listening on", queue_name)

                channel.basic_consume(
                    queue=queue_name,
                    on_message_callback=lambda c, m, p, b: self.handle_data(b),
                    auto_ack=True,
                )

                channel.start_consuming()
            except pika.exceptions.AMQPConnectionError:
                logging.error("RabbitMQ connection error", handlers=[])
            except Exception:
                log_traceback()
            time.sleep(2)
Пример #18
0
    def __init__(self, parent):
        self.parent = parent

        self.caspar_host = parent.channel_config.get("caspar_host", "localhost")
        self.caspar_port = int(parent.channel_config.get("caspar_port", 5250))
        self.caspar_channel = int(parent.channel_config.get("caspar_channel", 1))
        self.caspar_feed_layer = int(parent.channel_config.get("caspar_feed_layer", 10))

        self.current_item = False
        self.current_fname = False
        self.cued_item = False
        self.cued_fname = False
        self.cueing = False
        self.force_cue = False

        self.paused = False
        self.loop = False
        self.stalled = False

        self.fpos = self.fdur = 0
        self.cued_in = self.cued_out = self.current_in = self.current_out = 0

        self.bad_requests = 0
        self.request_time = self.recovery_time = time.time()

        if not self.connect():
            logging.error("Unable to connect CasparCG Server. Shutting down.")
            self.parent.shutdown()
            return

        Parser = get_info_parser(self.infc)
        self.parser = Parser(self.infc, self.caspar_channel)

        thread.start_new_thread(self.work, ())
Пример #19
0
    def on_accept(self):
        reply = QMessageBox.question(
            self,
            "Save changes?",
            "{}".format("\n".join(" - {}".format(meta_types[k].alias(
                config.get("language", "en"))) for k in self.form.changed)),
            QMessageBox.Yes | QMessageBox.No,
        )

        if reply == QMessageBox.Yes:
            pass
        else:
            logging.info("Save aborted")
            return

        response = api.set(
            objects=[a.id for a in self.objects],
            data={k: self.form[k]
                  for k in self.form.changed},
        )

        if not response:
            logging.error(response.message)

        self.response = True
        self.close()
Пример #20
0
 def create_subclip(self):
     if not self.subclips.isVisible():
         self.subclips.show()
     if (not (self.player.mark_in and self.player.mark_out)
         ) or self.player.mark_in >= self.player.mark_out:
         logging.error("Unable to create subclip. Invalid region selected.")
         return
     self.subclips.create_subclip(self.player.mark_in, self.player.mark_out)
Пример #21
0
 def order_callback(self, response):
     self.parent().setCursor(Qt.ArrowCursor)
     if not response:
         logging.error("Unable to change bin order: {}".format(
             response.message))
         return False
     self.load()
     return False
Пример #22
0
 def save_marks(self):
     if (self.player.mark_in and self.player.mark_out
             and self.player.mark_in >= self.player.mark_out):
         logging.error(
             "Unable to save marks. In point must precede out point")
     else:
         self.changed["mark_in"] = self.player.mark_in
         self.changed["mark_out"] = self.player.mark_out
Пример #23
0
    def on_main(self):
        if not self.import_dir:
            return

        if not os.path.isdir(self.import_dir):
            logging.error("Import directory does not exist. Shutting down.")
            self.import_path = False
            self.shutdown(no_restart=True)
            return

        db = DB()
        for import_file in get_files(self.import_dir, exts=self.exts):
            idec = import_file.base_name
            try:
                with import_file.open("rb") as f:
                    f.seek(0, 2)
                    fsize = f.tell()
            except IOError:
                logging.debug(f"Import file {import_file.base_name} is busy.")
                continue

            if not (import_file.path in self.filesizes
                    and self.filesizes[import_file.path] == fsize):
                self.filesizes[import_file.path] = fsize
                logging.debug(f"New file '{import_file.base_name}' detected")
                continue

            db.query(
                """
                SELECT meta FROM assets
                WHERE meta->>%s = %s
                """,
                [self.identifier, idec],
            )
            for (meta, ) in db.fetchall():
                asset = Asset(meta=meta, db=db)

                if not (asset["id_storage"] and asset["path"]):
                    mk_error(import_file, "This file has no target path.")
                    continue

                if self.versioning and os.path.exists(asset.file_path):
                    version_backup(asset)

                do_import(self, import_file, asset)
                break
            else:
                mk_error(import_file, "This file is not expected.")

        for fname in os.listdir(self.import_dir):
            if not fname.endswith(".error.txt"):
                continue
            idec = fname.replace(".error.txt", "")
            if idec not in [
                    os.path.splitext(f)[0] for f in os.listdir(self.import_dir)
            ]:
                os.remove(os.path.join(self.import_dir, fname))
Пример #24
0
def encode_to_UTF8(data):
    try:
        return data.encode('UTF-8')
    except UnicodeEncodeError as e:
        logging.error("Could not encode data to UTF-8 -- %s" % e)
        return False
    except Exception as e:
        raise (e)
        return False
Пример #25
0
    def load_callback(self, response):
        self.parent().setCursor(Qt.ArrowCursor)
        if not response:
            logging.error(response.message)
            return

        QApplication.processEvents()
        self.parent().setCursor(Qt.WaitCursor)
        self.beginResetModel()
        logging.info("Loading rundown. Please wait...")

        required_assets = []

        self.header_data = config["playout_channels"][self.id_channel].get(
            "rundown_columns", DEFAULT_COLUMNS)
        self.object_data = []
        self.event_ids = []

        i = 0
        for row in response.data:
            row["rundown_row"] = i
            if row["object_type"] == "event":
                self.object_data.append(Event(meta=row))
                i += 1
                self.event_ids.append(row["id"])
                if row["is_empty"]:
                    self.object_data.append(
                        Item(meta={
                            "title": "(Empty event)",
                            "id_bin": row["id_bin"]
                        }))
                    i += 1
            elif row["object_type"] == "item":
                item = Item(meta=row)
                item.id_channel = self.id_channel
                if row["id_asset"]:
                    item._asset = asset_cache.get(row["id_asset"])
                    required_assets.append(
                        [row["id_asset"], row["asset_mtime"]])
                else:
                    item._asset = False
                self.object_data.append(item)
                i += 1
            else:
                continue

        asset_cache.request(required_assets)

        self.endResetModel()
        self.parent().setCursor(Qt.ArrowCursor)
        logging.goodnews(
            "Rundown loaded in {:.03f}s".format(time.time() -
                                                self.load_start_time))

        if self.current_callback:
            self.current_callback()
Пример #26
0
def mk_error(fname, message):
    log_file_path = os.path.splitext(fname.path)[0] + ".error.txt"
    try:
        old_message = open(log_file_path).read()
    except Exception:
        old_message = ""
    if old_message != message:
        logging.error("{} : {}".format(fname.base_name, message))
        with open(log_file_path, "w") as f:
            f.write(message)
Пример #27
0
 def on_solve(self, solver):
     QApplication.processEvents()
     QApplication.setOverrideCursor(Qt.WaitCursor)
     response = api.solve(id_item=self.selected_objects[0]["id"],
                          solver=solver)
     QApplication.restoreOverrideCursor()
     if not response:
         logging.error(response.message)
     self.model().load()
     self.parent().main_window.scheduler.load()
Пример #28
0
    def start(self, **kwargs):
        progress_handler = kwargs.get("progress_handler", None)

        if not self.input_files:
            logging.error(
                "Unable to start transcoding. No input file specified.")
            return False

        if not self.outputs:
            logging.error(
                "Unable to start transcoding. No output profile specified.")
            return False

        cmd = ["-y"]

        for input_file in self.input_files:
            if input_file.input_args:
                cmd.extend(input_file.input_args)
            if self["mark_in"]:
                cmd.extend(["-ss", str(self["mark_in"])])
            cmd.extend(["-i", input_file.path])

        filter_chain = self.filter_chain
        cmd.extend(["-filter_complex", filter_chain])

        for i, output in enumerate(self.outputs):
            if output.has_video:
                cmd.extend(["-map", "[outv{}]".format(i)])

            for sink in output.audio_sinks:
                cmd.extend(["-map", sink])

            if output.has_video:
                cmd.extend(["-r", self["fps"]])
            if self["mark_out"]:
                cmd.extend(["-to", str(self["mark_out"])])

            cmd.extend(output.build())

        is_success = ffmpeg(*cmd,
                            debug=self["debug"],
                            progress_handler=progress_handler)

        if self["use_temp_file"]:
            if is_success:
                for output in self.outputs:
                    os.rename(output.temp_path, output.output_path)
            else:
                for output in self.outputs:
                    try:
                        os.remove(output.temp_path)
                    except Exception:
                        pass
        return is_success
Пример #29
0
def check_file_validity(asset, id_channel):
    path = asset.get_playout_full_path(id_channel)
    try:
        res = mediaprobe(path)
    except Exception:
        logging.error("Unable to read", path)
        return ObjectStatus.CORRUPTED, 0
    if not res:
        return ObjectStatus.CORRUPTED, 0
    if res["duration"]:
        return ObjectStatus.CREATING, res["duration"]
    return ObjectStatus.UNKNOWN, 0
Пример #30
0
    def __init__(self, **kwargs):
        super(FireflyApplication, self).__init__(sys.argv)
        self.app_state = {"name": "firefly", "title": f"Firefly {FIREFLY_VERSION}"}
        self.app_state_path = os.path.join(app_dir, f"{app_settings['name']}.appstate")
        self.setStyleSheet(app_skin)
        locale.setlocale(locale.LC_NUMERIC, "C")
        self.splash = QSplashScreen(pixlib["splash"])
        self.splash.show()

        # Which site we are running

        i = 0
        if "sites" in config:
            if len(config["sites"]) > 1:
                i = show_site_select_dialog()
            else:
                i = 0

        self.local_keys = list(config["sites"][i].keys())
        config.update(config["sites"][i])
        del config["sites"]

        self.app_state_path = os.path.join(
            app_dir, f"ffdata.{config['site_name']}.appstate"
        )
        self.auth_key_path = os.path.join(app_dir, f"ffdata.{config['site_name']}.key")

        # Login

        session_id = None
        try:
            session_id = open(self.auth_key_path).read()
        except FileNotFoundError:
            pass
        except Exception:
            log_traceback()
        config["session_id"] = session_id

        user_meta = check_login(self.splash)
        if not user_meta:
            logging.error("Unable to log in")
            sys.exit(0)
        user.meta = user_meta

        # Load settings and show main window
        self.splash_message("Loading site settings...")
        self.load_settings()
        self.splash_message("Loading filesystem...")
        load_filesystem()
        self.splash_message("Loading asset cache...")
        asset_cache.load()
        self.splash_message("Loading user workspace...")
        self.main_window = FireflyMainWindow(self, FireflyMainWidget)