def api_solve(**kwargs): id_item = kwargs.get("id_item", False) solver_name = kwargs.get("solver", False) items = kwargs.get("items", []) user = kwargs.get("user", anonymous) if not user.has_right("rundown_edit", anyval=True): return NebulaResponse(403) if id_item: items.append(id_item) if not (items and solver_name): return NebulaResponse( 400, "You must specify placeholder item ID and a solver name" ) Solver = get_solver(solver_name) if Solver is None: return NebulaResponse(500, "Unable to load the solver. Check logs for details") db = DB() for id_item in items: solver = Solver(Item(id_item, db=db), db=db) response = solver.main() if response.is_error: return response return response
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 cue(self, fname, item, layer=None, play=False, auto=True, loop=False, **kwargs): layer = layer or self.caspar_feed_layer query_list = ["PLAY" if play else "LOADBG"] query_list.append(f"{self.caspar_channel}-{layer}") query_list.append(fname) if auto: query_list.append("AUTO") if loop: query_list.append("LOOP") if item.mark_in(): query_list.append(f"SEEK {int(item.mark_in() * self.channel_fps)}") if item.mark_out(): query_list.append( f"LENGTH {int(item.duration * self.channel_fps)}") query = " ".join(query_list) self.cueing = fname self.cueing_item = item self.cueing_time = time.time() result = self.query(query) if result.is_error: message = f'Unable to cue "{fname}" {result.data}' self.cued_item = Item() self.cued_fname = False self.cueing = False self.cueing_item = False self.cueing_time = 0 return NebulaResponse(result.response, message) if play: self.cueing = False self.cueing_item = False self.cueing_time = 0 self.current_item = item self.current_fname = fname return NebulaResponse(200)
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 cue(self, fname, item, **kwargs): auto = kwargs.get("auto", True) layer = kwargs.get("layer", self.caspar_feed_layer) play = kwargs.get("play", False) loop = kwargs.get("loop", False) mark_in = item.mark_in() mark_out = item.mark_out() marks = "" if loop: marks += " LOOP" if mark_in: marks += " SEEK {}".format(int(mark_in * self.parser.seek_fps)) if mark_out and mark_out < item["duration"] and mark_out > mark_in: marks += " LENGTH {}".format( int((mark_out - mark_in) * self.parser.seek_fps) ) if play: q = "PLAY {}-{} {}{}".format(self.caspar_channel, layer, fname, marks) else: q = "LOADBG {}-{} {} {} {}".format( self.caspar_channel, layer, fname, ["", "AUTO"][auto], marks ) self.cueing = fname result = self.query(q) if result.is_error: message = 'Unable to cue "{}" {} - args: {}'.format( fname, result.data, str(kwargs) ) self.cued_item = Item() self.cued_fname = False self.cueing = False else: self.cued_item = item self.cued_fname = fname self.cued_in = mark_in * self.fps self.cued_out = mark_out * self.fps message = "Cued item {} ({})".format(self.cued_item, fname) return NebulaResponse(result.response, message)
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_osc_port = int( parent.channel_config.get("caspar_osc_port", 5253)) 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.should_run = True self.current_item = Item() self.current_fname = False self.cued_item = False self.cued_fname = False self.cueing = False self.cueing_time = 0 self.cueing_item = False self.stalled = False # To be updated based on CCG data self.channel_fps = self.fps self.paused = False self.loop = False self.pos = self.dur = 0 if not self.connect(): logging.error("Unable to connect CasparCG Server. Shutting down.") self.parent.shutdown() return self.caspar_data = CasparOSCServer(self.caspar_osc_port) self.lock = threading.Lock() self.work_thread = threading.Thread(target=self.work, args=()) self.work_thread.start()
def get_bin_first_item(id_bin, db=False): if not db: db = DB() db.query( """ SELECT id, meta FROM items WHERE id_bin=%s ORDER BY position LIMIT 1 """, [id_bin], ) for _, meta in db.fetchall(): return Item(meta=meta, db=db) return False
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
class CasparController(object): time_unit = "f" 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, ()) @property def id_channel(self): return self.parent.id_channel @property def host(self): return self.caspar_host @property def port(self): return self.caspar_port def connect(self): if not hasattr(self, "cmdc"): self.cmdc = CasparCG(self.host, self.port) self.infc = CasparCG(self.host, self.port) return self.cmdc.connect() and self.infc.connect() def query(self, *args, **kwargs): return self.cmdc.query(*args, **kwargs) @property def fps(self): return self.parent.fps @property def position(self): return int(self.fpos - self.current_in) @property def duration(self): if self.parent.current_live: return 0 dur = self.fdur if self.current_out > 0: dur -= dur - self.current_out if self.current_in > 0: dur -= self.current_in return dur def work(self): while True: try: self.main() time.sleep(0.01) except Exception: log_traceback() time.sleep(0.3) def main(self): info = self.parser.get_info(self.caspar_feed_layer) if not info: logging.warning("Channel {} update stat failed".format(self.id_channel)) self.bad_requests += 1 if self.bad_requests > 10: logging.error("Connection lost. Reconnecting...") if self.connect(): logging.goodnews("Connection estabilished") else: logging.error("Connection call failed") time.sleep(2) time.sleep(0.3) return else: self.request_time = time.time() self.bad_requests = 0 current_fname = info["current"] cued_fname = info["cued"] # # # Auto recovery # # if not current_fname and time.time() - self.recovery_time > 20: # self.parent.channel_recover() # return # self.recovery_time = time.time() if ( cued_fname and (not self.paused) and (info["pos"] == self.fpos) and not self.parent.current_live and self.cued_item and (not self.cued_item["run_mode"]) ): if self.stalled and self.stalled < time.time() - 5: logging.warning("Stalled for a long time") logging.warning("Taking stalled clip (pos: {})".format(self.fpos)) self.take() elif not self.stalled: logging.debug("Playback is stalled") self.stalled = time.time() elif self.stalled: logging.debug("No longer stalled") self.stalled = False self.fpos = info["pos"] self.fdur = info["dur"] # # Playlist advancing # advanced = False if ( self.cueing and self.cueing == current_fname and not cued_fname and not self.parent.cued_live ): logging.warning("Using short clip workaround") self.current_item = self.cued_item self.current_fname = current_fname self.current_in = self.cued_in self.current_out = self.cued_out self.cued_in = self.cued_out = 0 advanced = True self.cued_item = False self.cueing = False self.cued_fname = False elif (not cued_fname) and (current_fname) and not self.parent.cued_live: if current_fname == self.cued_fname: self.current_item = self.cued_item self.current_fname = self.cued_fname self.current_in = self.cued_in self.current_out = self.cued_out self.cued_in = self.cued_out = 0 advanced = True self.cued_item = False elif (not current_fname) and (not cued_fname) and self.parent.cued_live: self.current_item = self.cued_item self.current_fname = "LIVE" self.current_in = 0 self.current_out = 0 self.cued_in = self.cued_out = 0 advanced = True self.cued_item = False self.parent.on_live_enter() if advanced: try: self.parent.on_change() except Exception: log_traceback("Playout on_change failed") if self.current_item and not self.cued_item and not self.cueing: self.parent.cue_next() elif self.force_cue: logging.info("Forcing cue next") self.parent.cue_next() self.force_cue = False if self.cueing: if cued_fname == self.cued_fname: logging.debug("Cued", self.cueing) self.cueing = False else: logging.debug("Cueing", self.cueing) if ( self.cued_item and cued_fname and cued_fname != self.cued_fname and not self.cueing ): logging.warning( "Cue mismatch: IS: {} SHOULDBE: {}".format(cued_fname, self.cued_fname) ) self.cued_item = False try: self.parent.on_progress() except Exception: log_traceback("Playout on_main failed") self.current_fname = current_fname self.cued_fname = cued_fname def cue(self, fname, item, **kwargs): auto = kwargs.get("auto", True) layer = kwargs.get("layer", self.caspar_feed_layer) play = kwargs.get("play", False) loop = kwargs.get("loop", False) mark_in = item.mark_in() mark_out = item.mark_out() marks = "" if loop: marks += " LOOP" if mark_in: marks += " SEEK {}".format(int(mark_in * self.parser.seek_fps)) if mark_out and mark_out < item["duration"] and mark_out > mark_in: marks += " LENGTH {}".format( int((mark_out - mark_in) * self.parser.seek_fps) ) if play: q = "PLAY {}-{} {}{}".format(self.caspar_channel, layer, fname, marks) else: q = "LOADBG {}-{} {} {} {}".format( self.caspar_channel, layer, fname, ["", "AUTO"][auto], marks ) self.cueing = fname result = self.query(q) if result.is_error: message = 'Unable to cue "{}" {} - args: {}'.format( fname, result.data, str(kwargs) ) self.cued_item = Item() self.cued_fname = False self.cueing = False else: self.cued_item = item self.cued_fname = fname self.cued_in = mark_in * self.fps self.cued_out = mark_out * self.fps message = "Cued item {} ({})".format(self.cued_item, fname) return NebulaResponse(result.response, message) def clear(self, **kwargs): layer = kwargs.get("layer", self.caspar_feed_layer) result = self.query("CLEAR {}-{}".format(self.channel, layer)) return NebulaResponse(result.response, result.data) def take(self, **kwargs): layer = kwargs.get("layer", self.caspar_feed_layer) if not self.cued_item or self.cueing: return NebulaResponse(400, "Unable to take. No item is cued.") self.paused = False result = self.query("PLAY {}-{}".format(self.caspar_channel, layer)) if result.is_success: if self.parent.current_live: self.parent.on_live_leave() message = "Take OK" self.stalled = False self.paused = False else: message = "Take command failed: " + result.data return NebulaResponse(result.response, message) def retake(self, **kwargs): layer = kwargs.get("layer", self.caspar_feed_layer) if self.parent.current_live: return NebulaResponse(409, "Unable to retake live item") seekparam = str(int(self.current_item.mark_in() * self.fps)) if self.current_item.mark_out(): seekparam += " LENGTH {}".format( int( (self.current_item.mark_out() - self.current_item.mark_in()) * self.parser.seek_fps ) ) q = "PLAY {}-{} {} SEEK {}".format( self.caspar_channel, layer, self.current_fname, seekparam ) self.paused = False result = self.query(q) if result.is_success: message = "Retake OK" self.stalled = False self.paused = False self.parent.cue_next() else: message = "Take command failed: " + result.data return NebulaResponse(result.response, message) def freeze(self, **kwargs): layer = kwargs.get("layer", self.caspar_feed_layer) if self.parent.current_live: return NebulaResponse(409, "Unable to freeze live item") if not self.paused: q = "PAUSE {}-{}".format(self.caspar_channel, layer) message = "Playback paused" new_val = True else: if self.parser.protocol >= 2.07: q = "RESUME {}-{}".format(self.caspar_channel, layer) else: length = "LENGTH {}".format( int((self.current_out or self.fdur) - self.fpos) ) q = "PLAY {}-{} {} SEEK {} {}".format( self.caspar_channel, layer, self.current_fname, self.fpos, length ) message = "Playback resumed" new_val = False result = self.query(q) if result.is_success: self.paused = new_val if self.parser.protocol < 2.07 and not new_val: self.force_cue = True else: message = result.data return NebulaResponse(result.response, message) def abort(self, **kwargs): layer = kwargs.get("layer", self.caspar_feed_layer) if not self.cued_item: return NebulaResponse(400, "Unable to abort. No item is cued.") q = "LOAD {}-{} {}".format(self.caspar_channel, layer, self.cued_fname) if self.cued_item.mark_in(): q += " SEEK {}".format(int(self.cued_item.mark_in() * self.parser.seek_fps)) if self.cued_item.mark_out(): q += " LENGTH {}".format( int( (self.cued_item.mark_out() - self.cued_item.mark_in()) * self.parser.seek_fps ) ) result = self.query(q) if result.is_success: self.paused = True return NebulaResponse(result.response, result.data)
class CasparController(object): time_unit = "s" 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_osc_port = int( parent.channel_config.get("caspar_osc_port", 5253)) 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.should_run = True self.current_item = Item() self.current_fname = False self.cued_item = False self.cued_fname = False self.cueing = False self.cueing_time = 0 self.cueing_item = False self.stalled = False # To be updated based on CCG data self.channel_fps = self.fps self.paused = False self.loop = False self.pos = self.dur = 0 if not self.connect(): logging.error("Unable to connect CasparCG Server. Shutting down.") self.parent.shutdown() return self.caspar_data = CasparOSCServer(self.caspar_osc_port) self.lock = threading.Lock() self.work_thread = threading.Thread(target=self.work, args=()) self.work_thread.start() def shutdown(self): logging.info("Controller shutdown requested") self.should_run = False self.caspar_data.shutdown() def on_main(self): if time.time() - self.caspar_data.last_osc > 5: logging.warning("Waiting for OSC") @property def id_channel(self): return self.parent.id_channel @property def request_time(self): return time.time() @property def fps(self): return self.parent.fps @property def position(self) -> float: """Time position (seconds) of the clip currently playing""" if self.current_item: return self.pos - self.current_item.mark_in() return self.pos @property def duration(self) -> float: """Duration (seconds) of the clip currently playing""" if self.parent.current_live: return 0 return self.dur def connect(self): """Connect to a running CasparCG instance using AMCP protocol""" self.cmdc = CasparCG(self.caspar_host, self.caspar_port) return self.cmdc.connect() def query(self, *args, **kwargs) -> NebulaResponse: """Send an AMCP query to the CasparCG server""" return self.cmdc.query(*args, **kwargs) 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 main(self): channel = self.caspar_data[self.caspar_channel] if not channel: return layer = channel[self.caspar_feed_layer] if not layer: return foreground = layer["foreground"] background = layer["background"] current_fname = os.path.splitext(foreground.name)[0] cued_fname = os.path.splitext(background.name)[0] pos = foreground.position dur = foreground.duration self.channel_fps = channel.fps self.paused = foreground.paused self.loop = foreground.loop # if cued_fname and (not self.paused) and (pos == self.pos) and (not self.parent.current_live) and self.cued_item and (not self.cued_item["run_mode"]): # if self.stalled and self.stalled < time.time() - 5: # logging.warning("Stalled for a long time") # logging.warning("Taking stalled clip (pos: {})".format(self.pos)) # self.take() # elif not self.stalled: # logging.debug("Playback is stalled") # self.stalled = time.time() # elif self.stalled: # logging.debug("No longer stalled") # self.stalled = False self.pos = pos self.dur = dur # # Playlist advancing # advanced = False if self.parent.cued_live: if ((background.producer == "empty") and (foreground.producer != "empty") and not self.cueing): self.current_item = self.cued_item self.current_fname = "LIVE" advanced = True self.cued_item = False self.parent.on_live_enter() else: if (not cued_fname) and (current_fname): if current_fname == self.cued_fname: self.current_item = self.cued_item self.current_fname = self.cued_fname advanced = True self.cued_item = False if advanced and not self.cueing: try: self.parent.on_change() except Exception: log_traceback("Playout on_change failed") if self.current_item and not self.cued_item and not self.cueing: self.cueing = True if not self.parent.cue_next(): self.cueing = False if self.cueing: if cued_fname == self.cueing: logging.goodnews(f"Cued {self.cueing}") self.cued_item = self.cueing_item self.cueing_item = False self.cueing = False elif self.parent.cued_live: if background.producer != "empty": logging.goodnews(f"Cued {self.cueing}") self.cued_item = self.cueing_item self.cueing_item = False self.cueing = False else: logging.debug( f"Waiting for cue {self.cueing} (is {cued_fname})") if time.time() - self.cueing_time > 5 and self.current_item: logging.warning("Cueing again") self.cueing = False self.parent.cue_next() elif (not self.cueing and self.cued_item and cued_fname and cued_fname != self.cued_fname and not self.parent.cued_live): logging.error( f"Cue mismatch: IS: {cued_fname} SHOULDBE: {self.cued_fname}") self.cued_item = False self.current_fname = current_fname self.cued_fname = cued_fname try: self.parent.on_progress() except Exception: log_traceback("Playout on_progress failed") def cue(self, fname, item, layer=None, play=False, auto=True, loop=False, **kwargs): layer = layer or self.caspar_feed_layer query_list = ["PLAY" if play else "LOADBG"] query_list.append(f"{self.caspar_channel}-{layer}") query_list.append(fname) if auto: query_list.append("AUTO") if loop: query_list.append("LOOP") if item.mark_in(): query_list.append(f"SEEK {int(item.mark_in() * self.channel_fps)}") if item.mark_out(): query_list.append( f"LENGTH {int(item.duration * self.channel_fps)}") query = " ".join(query_list) self.cueing = fname self.cueing_item = item self.cueing_time = time.time() result = self.query(query) if result.is_error: message = f'Unable to cue "{fname}" {result.data}' self.cued_item = Item() self.cued_fname = False self.cueing = False self.cueing_item = False self.cueing_time = 0 return NebulaResponse(result.response, message) if play: self.cueing = False self.cueing_item = False self.cueing_time = 0 self.current_item = item self.current_fname = fname return NebulaResponse(200) def clear(self, layer=None): layer = layer or self.caspar_feed_layer result = self.query(f"CLEAR {self.channel}-{layer}") return NebulaResponse(result.response, result.data) def take(self, **kwargs): layer = kwargs.get("layer", self.caspar_feed_layer) if not self.cued_item or self.cueing: return NebulaResponse(400, "Unable to take. No item is cued.") result = self.query(f"PLAY {self.caspar_channel}-{layer}") if result.is_success: if self.parent.current_live: self.parent.on_live_leave() message = "Take OK" self.stalled = False else: message = "Take failed: {result.data}" return NebulaResponse(result.response, message) def retake(self, **kwargs): layer = kwargs.get("layer", self.caspar_feed_layer) if self.parent.current_live: return NebulaResponse(409, "Unable to retake live item") seekparams = "SEEK " + str( int(self.current_item.mark_in() * self.channel_fps)) if self.current_item.mark_out(): seekparams += " LENGTH " + str( int((self.current_item.mark_out() - self.current_item.mark_in()) * self.channel_fps)) query = f"PLAY {self.caspar_channel}-{layer} {self.current_fname} {seekparams}" result = self.query(query) if result.is_success: message = "Retake OK" self.stalled = False self.parent.cue_next() else: message = "Take command failed: " + result.data return NebulaResponse(result.response, message) def freeze(self, **kwargs): layer = kwargs.get("layer", self.caspar_feed_layer) if self.parent.current_live: return NebulaResponse(409, "Unable to freeze live item") if self.paused: query = f"RESUME {self.caspar_channel}-{layer}" message = "Playback resumed" else: query = f"PAUSE {self.caspar_channel}-{layer}" message = "Playback paused" result = self.query(query) return NebulaResponse(result.response, message) def abort(self, **kwargs): layer = kwargs.get("layer", self.caspar_feed_layer) if not self.cued_item: return NebulaResponse(400, "Unable to abort. No item is cued.") query = f"LOAD {self.caspar_channel}-{layer} {self.cued_fname}" if self.cued_item.mark_in(): seek = int(self.cued_item.mark_in() * self.channel_fps) query += f" SEEK {seek}" if self.cued_item.mark_out(): length = int( (self.cued_item.mark_out() - self.cued_item.mark_in()) * self.channel_fps) query += f" LENGTH {length}" result = self.query(query) return NebulaResponse(result.response, result.data) def set(self, key, value): if key == "loop": do_loop = int(str(value) in ["1", "True", "true"]) result = self.query( f"CALL {self.caspar_channel}-{self.caspar_feed_layer} LOOP {do_loop}" ) if self.current_item and bool( self.current_item["loop"] != bool(do_loop)): self.current_item["loop"] = bool(do_loop) self.current_item.save(notify=False) bin_refresh([self.current_item["id_bin"]], db=self.current_item.db) return NebulaResponse(result.response, f"SET LOOP: {result.data}") else: return NebulaResponse(400)
def cue(self, **kwargs): db = kwargs.get("db", DB()) lcache = kwargs.get("cache", cache) if "item" in kwargs and isinstance(kwargs["item"], Item): item = kwargs["item"] del kwargs["item"] elif "id_item" in kwargs: item = Item(int(kwargs["id_item"]), db=db, cache=lcache) item.asset del kwargs["id_item"] else: return NebulaResponse(400, "Unable to cue. No item specified") if not item: return NebulaResponse(404, f"Unable to cue. {item} does not exist") if item["item_role"] == "live": logging.info("Next is item is live") fname = self.channel_config.get("live_source", "EMPTY") response = self.controller.cue(fname, item, **kwargs) if response.is_success: self.cued_live = True return response if not item["id_asset"]: return NebulaResponse(400, f"Unable to cue virtual {item}") asset = item.asset playout_status = asset.get(self.status_key, DEFAULT_STATUS)["status"] kwargs["fname"] = kwargs["full_path"] = None if playout_status in [ ObjectStatus.ONLINE, ObjectStatus.CREATING, ObjectStatus.UNKNOWN, ]: kwargs["fname"] = asset.get_playout_name(self.id_channel) kwargs["full_path"] = asset.get_playout_full_path(self.id_channel) if (not kwargs["full_path"] and self.channel_config.get("allow_remote") and asset["status"] in (ObjectStatus.ONLINE, ObjectStatus.CREATING)): kwargs["fname"] = kwargs["full_path"] = asset.file_path kwargs["remote"] = True if not kwargs["full_path"]: state = ObjectStatus(playout_status).name return NebulaResponse(404, f"Unable to cue {state} playout file") kwargs["mark_in"] = item["mark_in"] kwargs["mark_out"] = item["mark_out"] if item["run_mode"] == 1: kwargs["auto"] = False else: kwargs["auto"] = True kwargs["loop"] = bool(item["loop"]) self.cued_live = False return self.controller.cue(item=item, **kwargs)
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_order(**kwargs): """ Changes order of items in bin/rundown, creates new items from assets """ id_channel = kwargs.get("id_channel", 0) id_bin = kwargs.get("id_bin", False) order = kwargs.get("order", []) db = kwargs.get("db", DB()) user = kwargs.get("user", anonymous) initiator = kwargs.get("initiator", None) if not user: return NebulaResponse(401) if not id_channel in config["playout_channels"]: return NebulaResponse(400, f"No such channel ID {id_channel}") playout_config = config["playout_channels"][id_channel] append_cond = playout_config.get("rundown_accepts", "True") if id_channel and not user.has_right("rundown_edit", id_channel): return NebulaResponse(403, "You are not allowed to edit this rundown") if not (id_bin and order): return NebulaResponse( 400, f'Bad "order" request<br>id_bin: {id_bin}<br>order: {order}') logging.info(f"{user} executes bin_order method") affected_bins = [id_bin] pos = 1 rlen = float(len(order)) for i, obj in enumerate(order): object_type = obj["object_type"] id_object = obj["id_object"] meta = obj["meta"] if object_type == "item": if not id_object: item = Item(db=db) item["id_asset"] = obj.get("id_asset", 0) item.meta.update(meta) else: item = Item(id_object, db=db) if not item["id_bin"]: logging.error( f"Attempted asset data insertion ({object_type} ID {id_object} {meta}) to item. This should never happen" ) continue if not item: logging.debug(f"Skipping {item}") continue if not item["id_bin"] in affected_bins: if item["id_bin"]: affected_bins.append(item["id_bin"]) elif object_type == "asset": asset = Asset(id_object, db=db) if not asset: logging.error( f"Unable to append {object_type} ID {id_object}. Asset does not exist" ) continue try: can_append = eval(append_cond) except Exception: log_traceback( "Unable to evaluate rundown accept condition: {append_cond}" ) continue if not can_append: logging.error( f"Unable to append {asset}. Does not match conditions.") continue item = Item(db=db) for key in meta: if key in ["id", "id_bin", "id_asset"]: continue item[key] = meta[key] item["id_asset"] = asset.id item.meta.update(meta) else: logging.error( f"Unable to append {object_type} ID {id_object} {meta}. Unexpected object" ) continue if not item or item["position"] != pos or item["id_bin"] != id_bin: item["position"] = pos item["id_bin"] = id_bin # bin_refresh called later should be enough to trigger rundown reload item.save(notify=False) pos += 1 # Update bin duration bin_refresh(affected_bins, db=db, initiator=initiator) return NebulaResponse(200)
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)