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 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 next_event(self): if not self._next_event: self.db.query( """ SELECT meta FROM events WHERE id_channel = %s AND start > %s ORDER BY start ASC LIMIT 1 """, [self.event["id_channel"], self.event["start"]], ) try: self._next_event = Event(meta=self.db.fetchall()[0][0], db=self.db) except IndexError: self._next_event = Event( meta={ "id_channel": self.event["id_channel"], "start": self.event["start"] + 3600, } ) return self._next_event
def on_init(self): self.id_channel = int(self.settings.find("channel").text) self.current_task = None self.capture = Capture(self) """ self, ingest_mode = self.settings.find("ingest_mode").text, # PRIMARY - Nahrava se do assetu, BACKUP - Nahrava se lokalne cache_dir = self.settings.find("cache_dir").text, bmd_device = self.settings.find("device").text, bmd_mode = self.settings.find("mode").text, bmd_input = self.settings.find("input").text ) """ # DEMO EVENT db = DB() e = Event(5529, db=db) e["start"] = time.time() + 3 e["stop"] = time.time() + 30 e.save()
def get_item_event(id_item, **kwargs): db = kwargs.get("db", DB()) db.query(""" SELECT e.id, e.meta FROM items AS i, events AS e WHERE e.id_magic = i.id_bin AND i.id = {} AND e.id_channel in ({}) """.format( id_item, ", ".join([str(f) for f in config["playout_channels"].keys()]))) for _, meta in db.fetchall(): return Event(meta=meta, db=db) return False
def get_day_events(id_channel, date, num_days=1): chconfig = config["playout_channels"][id_channel] start_time = datestr2ts(date, *chconfig.get("day_start", [6, 0])) end_time = start_time + (3600 * 24 * num_days) db = DB() db.query( """ SELECT id, meta FROM events WHERE id_channel=%s AND start > %s AND start < %s """, (id_channel, start_time, end_time), ) for _, meta in db.fetchall(): yield Event(meta=meta)
def bin_refresh(bins, **kwargs): bins = [b for b in bins if b] if not bins: return True db = kwargs.get("db", DB()) sender = kwargs.get("sender", False) for id_bin in bins: b = Bin(id_bin, db=db) b.save(notify=False) bq = ", ".join([str(b) for b in bins if b]) changed_events = [] db.query(f""" SELECT e.meta FROM events as e, channels AS c WHERE c.channel_type = 0 AND c.id = e.id_channel AND e.id_magic IN ({bq}) """) for (meta, ) in db.fetchall(): event = Event(meta=meta, db=db) if event.id not in changed_events: changed_events.append(event.id) logging.debug(f"Bins changed {bins}.", f"Initiator {kwargs.get('initiator', logging.user)}") messaging.send( "objects_changed", sender=sender, objects=bins, object_type="bin", initiator=kwargs.get("initiator", None), ) if changed_events: logging.debug(f"Events changed {bins}." f"Initiator {kwargs.get('initiator', logging.user)}") messaging.send( "objects_changed", sender=sender, objects=changed_events, object_type="event", initiator=kwargs.get("initiator", None), ) return True
def on_change(self): if not self.controller.current_item: return item = self.controller.current_item db = DB() self.current_asset = item.asset or Asset() self.current_event = item.event or Event() logging.info(f"Advanced to {item}") if self.last_run: db.query( """ UPDATE asrun SET stop = %s WHERE id = %s""", [int(time.time()), self.last_run], ) db.commit() if self.current_item: db.query( """ INSERT INTO asrun (id_channel, id_item, start) VALUES (%s, %s, %s) """, [self.id_channel, item.id, time.time()], ) self.last_run = db.lastid() db.commit() else: self.last_run = False for plugin in self.plugins: try: plugin.on_change() except Exception: log_traceback("Plugin on-change failed")
def on_main(self): now = int(time.time()) db = DB() db.query( "SELECT id_object, start, stop, id_magic FROM nx_events WHERE id_channel = %s AND start < %s AND stop > %s ORDER BY start LIMIT 1", (self.id_channel, now, now)) for id_event, start, stop, id_asset in db.fetchall(): event = Event(id_event, db=db) if not self.current_task: task = CaptureTask(event) self.current_task = task break elif self.current_task.id_event != id_event: logging.warning( "Another event should be ingested right now....") continue break else: self.current_task = None self.capture(self.current_task)
def get_next_item(item, **kwargs): db = kwargs.get("db", DB()) force = kwargs.get("force", False) if type(item) == int and item > 0: current_item = Item(item, db=db) elif isinstance(item, Item): current_item = item else: logging.error(f"Unexpected get_next_item argument {item}") return False logging.debug(f"Looking for an item following {current_item}") current_bin = Bin(current_item["id_bin"], db=db) items = current_bin.items if force == "prev": items.reverse() for item in items: if (force == "prev" and item["position"] < current_item["position"] ) or (force != "prev" and item["position"] > current_item["position"]): if item["item_role"] == "lead_out" and not force: logging.info("Cueing Lead In") for i, r in enumerate(current_bin.items): if r["item_role"] == "lead_in": return r else: next_item = current_bin.items[0] next_item.asset return next_item if item["run_mode"] == RunMode.RUN_SKIP: continue item.asset return item else: current_event = get_item_event(item.id, db=db) direction = ">" order = "ASC" if force == "prev": direction = "<" order = "DESC" db.query( f""" SELECT meta FROM events WHERE id_channel = %s and start {direction} %s ORDER BY start {order} LIMIT 1 """, [current_event["id_channel"], current_event["start"]], ) try: next_event = Event(meta=db.fetchall()[0][0], db=db) if not next_event.bin.items: logging.debug("Next playlist is empty") raise Exception if next_event["run_mode"] and not kwargs.get( "force_next_event", False): logging.debug("Next playlist run mode is not auto") raise Exception if force == "prev": next_item = next_event.bin.items[-1] else: next_item = next_event.bin.items[0] next_item.asset return next_item except Exception: logging.info("Looping current playlist") next_item = current_bin.items[0] next_item.asset return next_item
def on_main(self): """ This method checks if the following event should start automatically at given time. It does not handle AUTO playlist advancing """ if not hasattr(self, "controller"): return if hasattr(self.controller, "on_main"): self.controller.on_main() current_item = self.controller.current_item # YES. CURRENT if not current_item: return db = DB() current_event = get_item_event(current_item.id, db=db) if not current_event: logging.warning("Unable to fetch the current event") return db.query( """ SELECT DISTINCT(e.id), e.meta, e.start FROM events AS e, items AS i WHERE e.id_channel = %s AND e.start > %s AND e.start <= %s AND i.id_bin = e.id_magic ORDER BY e.start ASC LIMIT 1 """, [self.id_channel, current_event["start"], time.time()], ) try: next_event = Event(meta=db.fetchall()[0][1], db=db) except IndexError: self.auto_event = False return if self.auto_event == next_event.id: return run_mode = int(next_event["run_mode"]) or RunMode.RUN_AUTO if not run_mode: return elif not next_event.bin.items: return elif run_mode == RunMode.RUN_MANUAL: pass # ????? elif run_mode == RunMode.RUN_SOFT: logging.info("Soft cue", next_event) # if current item is live, take next block/lead out automatically play = self.current_live for i, r in enumerate(current_event.bin.items): if r["item_role"] == "lead_out": try: self.cue( id_channel=self.id_channel, id_item=current_event.bin.items[i + 1].id, db=db, play=play, ) self.auto_event = next_event.id break except IndexError: pass else: try: id_item = next_event.bin.items[0].id except KeyError: id_item = 0 if not self.controller.cued_item: return if id_item != self.controller.cued_item.id: self.cue(id_channel=self.id_channel, id_item=id_item, db=db) self.auto_event = next_event.id return elif run_mode == RunMode.RUN_HARD: logging.info("Hard cue", next_event) id_item = next_event.bin.items[0].id self.cue(id_channel=self.id_channel, id_item=id_item, play=True, db=db) self.auto_event = next_event.id return
def get_rundown(id_channel, start_time=False, end_time=False, db=False): """Get a rundown.""" db = db or DB() channel_config = config["playout_channels"][id_channel] if not start_time: # default today sh, sm = channel_config.get("day_start", [6, 0]) rundown_date = time.strftime("%Y-%m-%d", time.localtime(time.time())) start_time = datestr2ts(rundown_date, hh=sh, mm=sm) end_time = end_time or start_time + (3600 * 24) item_runs = get_item_runs(id_channel, start_time, end_time, db=db) if channel_config.get("send_action", False): db.query( """SELECT id_asset FROM jobs WHERE id_action=%s AND status in (0, 5) """, [channel_config["send_action"]], ) pending_assets = [r[0] for r in db.fetchall()] else: pending_assets = [] db.query( """ SELECT e.id, e.meta, i.meta, a.meta FROM events AS e LEFT JOIN items AS i ON e.id_magic = i.id_bin LEFT JOIN assets AS a ON i.id_asset = a.id WHERE e.id_channel = %s AND e.start >= %s AND e.start < %s ORDER BY e.start ASC, i.position ASC, i.id ASC """, (id_channel, start_time, end_time), ) current_event_id = None event = None ts_broadcast = ts_scheduled = 0 pskey = "playout_status/{}".format(id_channel) for id_event, emeta, imeta, ameta in db.fetchall() + [ (-1, None, None, None) ]: if id_event != current_event_id: if event: yield event if not event.items: ts_broadcast = 0 if id_event == -1: break event = Event(meta=emeta) event.items = [] current_event_id = id_event rundown_event_asset = event.meta.get("id_asset", False) if event["run_mode"]: ts_broadcast = 0 event.meta["rundown_scheduled"] = ts_scheduled = event["start"] event.meta["rundown_broadcast"] = ts_broadcast = (ts_broadcast or ts_scheduled) if imeta: item = Item(meta=imeta, db=db) if ameta: asset = Asset(meta=ameta, db=db) if ameta else False item._asset = asset else: asset = False as_start, as_stop = item_runs.get(item.id, (0, 0)) airstatus = 0 if as_start: ts_broadcast = as_start if as_stop: airstatus = ObjectStatus.AIRED else: airstatus = ObjectStatus.ONAIR item.meta["asset_mtime"] = asset["mtime"] if asset else 0 item.meta["rundown_scheduled"] = ts_scheduled item.meta["rundown_broadcast"] = ts_broadcast item.meta["rundown_difference"] = ts_broadcast - ts_scheduled if rundown_event_asset: item.meta["rundown_event_asset"] = rundown_event_asset istatus = 0 if not asset: istatus = ObjectStatus.ONLINE elif airstatus: istatus = airstatus elif asset["status"] == ObjectStatus.OFFLINE: istatus = ObjectStatus.OFFLINE elif pskey not in asset.meta: istatus = ObjectStatus.REMOTE elif asset[pskey]["status"] == ObjectStatus.OFFLINE: istatus = ObjectStatus.REMOTE elif asset[pskey]["status"] == ObjectStatus.ONLINE: istatus = ObjectStatus.ONLINE elif asset[pskey]["status"] == ObjectStatus.CORRUPTED: istatus = ObjectStatus.CORRUPTED else: istatus = ObjectStatus.UNKNOWN item.meta["status"] = istatus if asset and asset.id in pending_assets: item.meta["transfer_progress"] = -1 if item["run_mode"] != RunMode.RUN_SKIP: ts_scheduled += item.duration ts_broadcast += item.duration event.items.append(item)
def api_schedule(**kwargs): id_channel = kwargs.get("id_channel", 0) start_time = kwargs.get("start_time", 0) end_time = kwargs.get("end_time", 0) events = kwargs.get("events", []) # Events to add/update delete = kwargs.get("delete", []) # Event ids to delete db = kwargs.get("db", DB()) user = kwargs.get("user", anonymous) initiator = kwargs.get("initiator", None) try: id_channel = int(id_channel) except ValueError: return NebulaResponse(400, "id_channel must be an integer") try: start_time = int(start_time) except ValueError: return NebulaResponse(400, "start_time must be an integer") try: end_time = int(end_time) except ValueError: return NebulaResponse(400, "end_time must be an integer") if not id_channel or id_channel not in config["playout_channels"]: return NebulaResponse(400, f"Unknown playout channel ID {id_channel}") changed_event_ids = [] # # Delete events # for id_event in delete: if not user.has_right("scheduler_edit", id_channel): return NebulaResponse(403, "You are not allowed to edit this channel") event = Event(id_event, db=db) if not event: logging.warning(f"Unable to delete non existent event ID {id_event}") continue try: event.bin.delete() except psycopg2.IntegrityError: return NebulaResponse(423, f"Unable to delete {event}. Already aired.") else: event.delete() changed_event_ids.append(event.id) # # Create / update events # for event_data in events: if not user.has_right("scheduler_edit", id_channel): return NebulaResponse(423, "You are not allowed to edit this channel") id_event = event_data.get("id", False) db.query( "SELECT meta FROM events WHERE id_channel=%s and start=%s", [id_channel, event_data["start"]], ) try: event_at_pos_meta = db.fetchall()[0][0] event_at_pos = Event(meta=event_at_pos_meta, db=db) except IndexError: event_at_pos = False if id_event: logging.debug(f"Updating event ID {id_event}") event = Event(id_event, db=db) if not event: logging.warning(f"No such event ID {id_event}") continue pbin = event.bin elif event_at_pos: event = event_at_pos pbin = event.bin else: logging.debug("Creating new event") event = Event(db=db) pbin = Bin(db=db) pbin.save() logging.debug("Saved", pbin) event["id_magic"] = pbin.id event["id_channel"] = id_channel id_asset = event_data.get("id_asset", False) if id_asset and id_asset != event["id_asset"]: asset = Asset(id_asset, db=db) if asset: logging.info(f"Replacing event primary asset with {asset}") pbin.delete_children() pbin.items = [] item = Item(db=db) item["id_asset"] = asset.id item["position"] = 0 item["id_bin"] = pbin.id item._asset = asset item.save() pbin.append(item) pbin.save() event["id_asset"] = asset.id for key in meta_types: if meta_types[key]["ns"] != "m": continue if key in asset.meta: event[key] = asset[key] for key in event_data: if key == "id_magic" and not event_data[key]: continue if key == "_items": for item_data in event_data["_items"]: if not pbin.items: start_pos = 0 else: start_pos = pbin.items[-1]["position"] try: pos = int(item_data["position"]) except KeyError: pos = 0 item = Item(meta=item_data, db=db) item["position"] = start_pos + pos item["id_bin"] = pbin.id item.save() continue event[key] = event_data[key] changed_event_ids.append(event.id) event.save(notify=False) if changed_event_ids: messaging.send( "objects_changed", objects=changed_event_ids, object_type="event", initiator=initiator, ) # # Return existing events # # TODO: ACL scheduler view result = [] if start_time and end_time: logging.debug( f"Requested events of channel {id_channel} " f"from {format_time(start_time)} to {format_time(end_time)}" ) db.query( """ SELECT e.meta, o.meta FROM events AS e, bins AS o WHERE e.id_channel=%s AND e.start > %s AND e.start < %s AND e.id_magic = o.id ORDER BY start ASC""", [id_channel, start_time, end_time], ) res = db.fetchall() db.query( """ SELECT e.meta, o.meta FROM events AS e, bins AS o WHERE e.id_channel=%s AND start <= %s AND e.id_magic = o.id ORDER BY start DESC LIMIT 1""", [id_channel, start_time], ) res = db.fetchall() + res for event_meta, alt_meta in res: ebin = Bin(meta=alt_meta, db=db) if "duration" in alt_meta.keys(): event_meta["duration"] = ebin.duration result.append(event_meta) return NebulaResponse(200, data=result)