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 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 kill_service(self, pid=False, id_service=False): if id_service in self.services: pid = self.services[id_service][0].pid if pid == os.getpid() or pid == 0: return logging.info(f"Attempting to kill PID {pid}") os.system(os.path.join(config["nebula_root"], "support", f"kill_tree.sh {pid}"))
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 selectionChanged(self, selected, deselected): rows = [] self.selected_objects = [] tot_dur = 0 for idx in self.selectionModel().selectedIndexes(): row = idx.row() if row in rows: continue rows.append(row) obj = self.model().object_data[row] self.selected_objects.append(obj) if obj.object_type in ["asset", "item"]: tot_dur += obj.duration if self.selected_objects and self.focus_enabled: self.parent().main_window.focus(self.selected_objects[0]) if (len(self.selected_objects) == 1 and self.selected_objects[0].object_type == "item" and self.selected_objects[0]["id_asset"]): asset = self.selected_objects[0].asset times = len([ obj for obj in self.model().object_data if obj.object_type == "item" and obj["id_asset"] == asset.id ]) logging.info("{} is scheduled {}x in this rundown".format( asset, times)) if len(self.selected_objects) > 1 and tot_dur: logging.info("{} objects selected. Total duration {}".format( len(self.selected_objects), s2time(tot_dur))) super(FireflyView, self).selectionChanged(selected, deselected)
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 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 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 on_activate(self, mi): obj = self.model().object_data[mi.row()] key = self.model().header_data[mi.column()] val = obj.show(key) QApplication.clipboard().setText(str(val)) logging.info(f'Copied "{val}" to clipboard')
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 on_init(self): if not config["playout_channels"]: logging.error("No playout channel configured") self.shutdown(no_restart=True) return try: self.id_channel = int(self.settings.find("id_channel").text) self.channel_config = config["playout_channels"][self.id_channel] except Exception: logging.error("Invalid channel specified") self.shutdown(no_restart=True) return self.fps = float(self.channel_config.get("fps", 25.0)) self.current_asset = Asset() self.current_event = Event() self.last_run = False self.last_info = 0 self.current_live = False self.cued_live = False self.auto_event = 0 self.status_key = f"playout_status/{self.id_channel}" self.plugins = PlayoutPlugins(self) self.controller = create_controller(self) if not self.controller: logging.error("Invalid controller specified") self.shutdown(no_restart=True) return port = int(self.channel_config.get("controller_port", 42100)) logging.info(f"Using port {port} for the HTTP interface.") self.server = HTTPServer(("", port), PlayoutRequestHandler) self.server.service = self self.server.methods = { "take": self.take, "cue": self.cue, "cue_forward": self.cue_forward, "cue_backward": self.cue_backward, "freeze": self.freeze, "set": self.set, "retake": self.retake, "abort": self.abort, "stat": self.stat, "plugin_list": self.plugin_list, "plugin_exec": self.plugin_exec, "recover": self.channel_recover, } self.server_thread = threading.Thread(target=self.server.serve_forever, args=(), daemon=True) self.server_thread.start() self.plugins.load() self.on_progress()
def work(self): while self.should_run: try: self.main() except Exception: log_traceback() time.sleep(1 / self.fps) logging.info("Controller work thread shutdown")
def start(self): self.splash.hide() try: self.exec_() except Exception: log_traceback() logging.info("Shutting down") self.on_exit()
def on_delete(self): items = list( set([ obj.id for obj in self.selected_objects if obj.object_type == "item" ])) events = list( set([ obj.id for obj in self.selected_objects if obj.object_type == "event" ])) if items and not self.parent().can_edit: logging.error("You are not allowed to modify this rundown items") return elif events and not self.parent().can_schedule: logging.error("You are not allowed to modify this rundown blocks") return if events or len(items) > 10: ret = QMessageBox.question( self, "Delete", "Do you REALLY want to delete " f"{len(items)} items and {len(events)} events?\n" "This operation CANNOT be undone", QMessageBox.Yes | QMessageBox.No, ) if ret != QMessageBox.Yes: return if items: QApplication.processEvents() QApplication.setOverrideCursor(Qt.WaitCursor) response = api.delete(object_type="item", objects=items) QApplication.restoreOverrideCursor() if not response: logging.error(response.message) return else: logging.info("Item deleted: {}".format(response.message)) if events: QApplication.processEvents() QApplication.setOverrideCursor(Qt.WaitCursor) response = api.schedule(delete=events, id_channel=self.parent().id_channel) QApplication.restoreOverrideCursor() if not response: logging.error(response.message) return else: logging.info("Event deleted: {}".format(response.message)) self.selectionModel().clear() self.model().load() self.parent().main_window.scheduler.refresh_events(events)
def run_forever(self): try: logging.info("Listening on port %d for clients.." % self.port) self.serve_forever() except KeyboardInterrupt: self.server_close() logging.info("Server terminated.") except Exception as e: log_traceback("ERROR: WebSocketsServer: " + str(e))
def shutdown(self, no_restart=False): logging.info("Shutting down") if no_restart: db = DB() db.query("UPDATE services SET autostart=FALSE WHERE id=%s", [self.id_service]) db.commit() self.on_shutdown() sys.exit(0)
def __init__(self, osc_port=5253): self.osc_port = osc_port self.channels = {} self.last_osc = time.time() logging.info(f"Starting OSC listener on port {self.osc_port}") self.osc_server = OSCServer("", self.osc_port, self.handle_osc) self.osc_thread = threading.Thread(target=self.serve_forever, args=()) self.osc_thread.name = "OSC Server" self.osc_thread.start()
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 __init__(self, parent, **kwargs): super(FireflyInteger, self).__init__(parent) self.setFocusPolicy(Qt.StrongFocus) self.setMinimum(kwargs.get("min", 0)) self.setMaximum(kwargs.get("max", 99999)) if kwargs.get("hide_null"): logging.info("HIDE NULL") self.setMinimum(0) self.setSpecialValueText(" ") self.setSingleStep(1) self.default = self.get_value()
def __init__(self, parent, **kwargs): super(FireflyNumeric, self).__init__(parent) self.setFocusPolicy(Qt.StrongFocus) self.setMinimum(kwargs.get("min", -99999)) self.setMaximum(kwargs.get("max", 99999)) if kwargs.get("hide_null"): logging.info("HIDE NULL") self.setMinimum(0) self.setSpecialValueText(" ") # TODO: custom step (default 1, allow floats) self.default = self.get_value()
def run(self): self.is_running = True logging.info(f"Starting {self.__class__.__name__}") while self.should_run: try: self.main() except Exception: log_traceback() self.first_run = False time.sleep(2) self.on_shutdown() self.is_running = False
def delete(self): if not self.id: return logging.info(f"Deleting {self}") cache.delete(self.cache_key) self.delete_children() self.db.query("DELETE FROM {} WHERE id=%s".format(self.table_name), [self.id]) self.db.query( "DELETE FROM ft WHERE object_type=%s AND id=%s", [self.object_type_id, self.id], ) self.db.commit()
def load(self): self.clear() self.update({ "get": api_get, "set": api_set, "browse": api_browse, "delete": api_delete, "settings": api_settings, "rundown": api_rundown, "order": api_order, "schedule": api_schedule, "jobs": api_jobs, "playout": api_playout, "actions": api_actions, "send": api_send, "solve": api_solve, "system": api_system, }) logging.info("Reloading API methods") apidir = get_plugin_path("api") if not apidir: return for plugin_entry in os.listdir(apidir): entry_path = os.path.join(apidir, plugin_entry) if os.path.isdir(entry_path): plugin_module_path = os.path.join(entry_path, plugin_entry + ".py") if not os.path.exists(plugin_module_path): continue elif not os.path.splitext(plugin_entry)[1] == ".py": continue else: plugin_module_path = os.path.join(apidir, plugin_entry) plugin_module_path = FileObject(plugin_module_path) plugin_name = plugin_module_path.base_name try: py_mod = imp.load_source(plugin_name, plugin_module_path.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_name}") continue plugin = py_mod.Plugin() logging.info(f"Loaded plugin {plugin_name} ({plugin_module_path})") self[plugin_name] = plugin
def save(self): if len(self.data) > CACHE_LIMIT: to_rm = list(self.data.keys()) to_rm.sort(key=lambda x: self.data[x].meta.get("_last_access", 0)) for t in to_rm[:-CACHE_LIMIT]: del self.data[t] logging.info("Saving {} assets to local cache".format(len(self.data))) start_time = time.time() data = [asset.meta for asset in self.data.values()] with open(self.cache_path, "w") as f: json.dump(data, f) logging.debug("Cache updated in {:.03f}s".format(time.time() - start_time))
def start_service(self, id_service, title, db=False): proc_cmd = [ os.path.join(config["nebula_root"], "manage.py"), "run", str(id_service), '"{}"'.format(title), ] if config.get("daemon_mode"): proc_cmd.append("--daemon") logging.info(f"Starting service ID {id_service} ({title})") self.services[id_service] = [ subprocess.Popen(proc_cmd, cwd=config["nebula_root"]), title, ]
def block_split(self, tc): if tc <= self.event["start"] or tc >= self.next_event["start"]: logging.error( "Timecode of block split must be between " "the current and next event start times" ) return False logging.info(f"Splitting {self.event} at {format_time(tc)}") logging.info( "Next event is {} at {}".format( self.next_event, self.next_event.show("start") ) ) new_bin = Bin(db=self.db) new_bin.save(notify=False) new_placeholder = Item(db=self.db) new_placeholder["id_bin"] = new_bin.id new_placeholder["position"] = 0 for key in self.placeholder.meta.keys(): if key not in ["id_bin", "position", "id_asset", "id"]: new_placeholder[key] = self.placeholder[key] new_placeholder.save(notify=False) new_bin.append(new_placeholder) new_bin.save(notify=False) new_event = Event(db=self.db) new_event["id_channel"] = self.event["id_channel"] new_event["title"] = "Split block" new_event["start"] = tc new_event["id_magic"] = new_bin.id new_event.save(notify=False) self._needed_duration = None self._next_event = None self.solve_next = new_placeholder if new_bin.id not in self.affected_bins: self.affected_bins.append(new_bin.id) return True
def execute(self, name): data = {} for slot in self.slots: data[slot] = self.slots[slot].get_value() response = api.playout( action="plugin_exec", id_channel=self.id_channel, id_plugin=self.id_plugin, action_name=name, data=json.dumps(data), ) if response: logging.info(f"{self.title} action '{name}' executed succesfully.") else: logging.error( f"[PLUGINS] Plugin error {response.response}\n\n{response.message}" )
def main(self, debug=False, counter=0): logging.info("Solving {}".format(self.placeholder)) message = "Solver returned no items. Keeping placeholder." try: for new_item in self.solve(): self.new_items.append(new_item) if debug: logging.debug("Appending {}".format(new_item.asset)) except Exception: message = log_traceback("Error occured during solving") return NebulaResponse(501, message) if debug: return NebulaResponse(202) if not self.new_items: return NebulaResponse(204, message) i = 0 for item in self.bin.items: i += 1 if item.id == self.placeholder.id: item.delete() for new_item in self.new_items: i += 1 new_item["id_bin"] = self.bin.id new_item["position"] = i new_item.save(notify=False) if item["position"] != i: item["position"] = i item.save(notify=False) if self.bin.id not in self.affected_bins: self.affected_bins.append(self.bin.id) if self.solve_next: self.init_solver(self.solve_next) return self.main(debug=debug, counter=len(self.new_items) + counter) bin_refresh(self.affected_bins, db=self.db) return NebulaResponse( 200, "Created {} new items".format(len(self.new_items) + counter) )
def listen(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) addr = config.get("seismic_addr", "224.168.1.1") port = int(config.get("seismic_port", 42005)) try: firstoctet = int(addr.split(".")[0]) is_multicast = firstoctet >= 224 except ValueError: is_multicast = False if is_multicast: logging.info(f"Starting multicast listener {addr}:{port}") sock.bind(("0.0.0.0", port)) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255) sock.setsockopt( socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(addr) + socket.inet_aton("0.0.0.0"), ) else: logging.info(f"Starting unicast listener {addr}:{port}") sock.bind((addr, port)) sock.settimeout(1) while True: try: data, _ = sock.recvfrom(4092) except (socket.error): continue try: message = SeismicMessage(json.loads(data.decode())) except Exception: continue if message.site_name != config["site_name"]: continue if message.method == "log": log_message(message)
def api_send(**kwargs): objects = kwargs.get("objects") or kwargs.get( "ids", []) # TODO: ids is deprecated. use objects instead id_action = kwargs.get("id_action", False) settings = kwargs.get("settings", {}) db = kwargs.get("db", DB()) user = kwargs.get("user", anonymous) restart_existing = kwargs.get("restart_existing", True) restart_running = kwargs.get("restart_running", False) if not user.has_right("job_control", anyval=True): return NebulaResponse(403, "You are not allowed to execute this action") # TODO: Better ACL if not id_action: return NebulaResponse(400, "No valid action selected") if not objects: return NebulaResponse(400, "No asset selected") if not user.has_right("job_control", id_action): return NebulaResponse(400, "You are not allowed to start this action") logging.info( "{} is starting action {} for the following assets: {}".format( user, id_action, ", ".join([str(i) for i in objects]))) for id_object in objects: send_to( id_object, id_action, settings=settings, id_user=user.id, restart_existing=restart_existing, restart_running=restart_running, db=db, ) return NebulaResponse( 200, "Starting {} job{}".format(len(objects), "s" if len(objects) > 1 else ""))