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)
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
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, )
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()
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()
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", "/"))
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
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")
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}")
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
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")
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()
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
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)
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()
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()
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)
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, ())
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()
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)
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
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
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))
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
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()
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)
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()
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
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
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)