from pypofetch import PypoFetch from telnetliquidsoap import TelnetLiquidsoap from datetime import datetime from datetime import timedelta import eventtypes import time class PypoLiquidsoap(): def __init__(self, logger, telnet_lock, host, port): self.logger = logger self.liq_queue_tracker = { "s0": None, "s1": None, "s2": None, "s3": None, } self.telnet_liquidsoap = TelnetLiquidsoap(telnet_lock, \ logger,\ host,\ port,\ self.liq_queue_tracker.keys()) def get_telnet_dispatcher(self): return self.telnet_liquidsoap def play(self, media_item): if media_item["type"] == eventtypes.FILE: self.handle_file_type(media_item) elif media_item["type"] == eventtypes.EVENT: self.handle_event_type(media_item) elif media_item["type"] == eventtypes.STREAM_BUFFER_START: self.telnet_liquidsoap.start_web_stream_buffer(media_item) elif media_item["type"] == eventtypes.STREAM_OUTPUT_START: if media_item['row_id'] != self.telnet_liquidsoap.current_prebuffering_stream_id: #this is called if the stream wasn't scheduled sufficiently ahead of time #so that the prebuffering stage could take effect. Let's do the prebuffering now. self.telnet_liquidsoap.start_web_stream_buffer(media_item) self.telnet_liquidsoap.start_web_stream(media_item) elif media_item['type'] == eventtypes.STREAM_BUFFER_END: self.telnet_liquidsoap.stop_web_stream_buffer() elif media_item['type'] == eventtypes.STREAM_OUTPUT_END: self.telnet_liquidsoap.stop_web_stream_output() else: raise UnknownMediaItemType(str(media_item)) def handle_file_type(self, media_item): """ Wait maximum 5 seconds (50 iterations) for file to become ready, otherwise give up on it. """ iter_num = 0 while not media_item['file_ready'] and iter_num < 50: time.sleep(0.1) iter_num += 1 if media_item['file_ready']: available_queue = self.find_available_queue() try: self.telnet_liquidsoap.queue_push(available_queue, media_item) self.liq_queue_tracker[available_queue] = media_item except Exception as e: self.logger.error(e) raise else: self.logger.warn("File %s did not become ready in less than 5 seconds. Skipping...", media_item['dst']) def handle_event_type(self, media_item): if media_item['event_type'] == "kick_out": self.telnet_liquidsoap.disconnect_source("live_dj") elif media_item['event_type'] == "switch_off": self.telnet_liquidsoap.switch_source("live_dj", "off") def is_media_item_finished(self, media_item): if media_item is None: return True else: return datetime.utcnow() > media_item['end'] def find_available_queue(self): available_queue = None for i in self.liq_queue_tracker: mi = self.liq_queue_tracker[i] if mi == None or self.is_media_item_finished(mi): #queue "i" is available. Push to this queue available_queue = i if available_queue == None: raise NoQueueAvailableException() return available_queue def verify_correct_present_media(self, scheduled_now): #verify whether Liquidsoap is currently playing the correct files. #if we find an item that Liquidsoap is not playing, then push it #into one of Liquidsoap's queues. If Liquidsoap is already playing #it do nothing. If Liquidsoap is playing a track that isn't in #currently_playing then stop it. #Check for Liquidsoap media we should source.skip #get liquidsoap items for each queue. Since each queue can only have one #item, we should have a max of 8 items. #2013-03-21-22-56-00_0: { #id: 1, #type: "stream_output_start", #row_id: 41, #uri: "http://stream2.radioblackout.org:80/blackout.ogg", #start: "2013-03-21-22-56-00", #end: "2013-03-21-23-26-00", #show_name: "Untitled Show", #independent_event: true #}, try: scheduled_now_files = \ filter(lambda x: x["type"] == eventtypes.FILE, scheduled_now) scheduled_now_webstream = \ filter(lambda x: x["type"] == eventtypes.STREAM_OUTPUT_START, \ scheduled_now) schedule_ids = set(map(lambda x: x["row_id"], scheduled_now_files)) row_id_map = {} liq_queue_ids = set() for i in self.liq_queue_tracker: mi = self.liq_queue_tracker[i] if not self.is_media_item_finished(mi): liq_queue_ids.add(mi["row_id"]) row_id_map[mi["row_id"]] = mi to_be_removed = set() to_be_added = set() #Iterate over the new files, and compare them to currently scheduled #tracks. If already in liquidsoap queue still need to make sure they don't #have different attributes #if replay gain changes, it shouldn't change the amplification of the currently playing song for i in scheduled_now_files: if i["row_id"] in row_id_map: mi = row_id_map[i["row_id"]] correct = mi['start'] == i['start'] and \ mi['end'] == i['end'] and \ mi['row_id'] == i['row_id'] if not correct: #need to re-add self.logger.info("Track %s found to have new attr." % i) to_be_removed.add(i["row_id"]) to_be_added.add(i["row_id"]) to_be_removed.update(liq_queue_ids - schedule_ids) to_be_added.update(schedule_ids - liq_queue_ids) if to_be_removed: self.logger.info("Need to remove items from Liquidsoap: %s" % \ to_be_removed) #remove files from Liquidsoap's queue for i in self.liq_queue_tracker: mi = self.liq_queue_tracker[i] if mi is not None and mi["row_id"] in to_be_removed: self.stop(i) if to_be_added: self.logger.info("Need to add items to Liquidsoap *now*: %s" % \ to_be_added) for i in scheduled_now_files: if i["row_id"] in to_be_added: self.modify_cue_point(i) self.play(i) #handle webstreams current_stream_id = self.telnet_liquidsoap.get_current_stream_id() if scheduled_now_webstream: if int(current_stream_id) != int(scheduled_now_webstream[0]["row_id"]): self.play(scheduled_now_webstream[0]) elif current_stream_id != "-1": #something is playing and it shouldn't be. self.telnet_liquidsoap.stop_web_stream_buffer() self.telnet_liquidsoap.stop_web_stream_output() except KeyError as e: self.logger.error("Error: Malformed event in schedule. " + str(e)) def stop(self, queue): self.telnet_liquidsoap.queue_remove(queue) self.liq_queue_tracker[queue] = None def is_file(self, media_item): return media_item["type"] == eventtypes.FILE def clear_queue_tracker(self): for i in self.liq_queue_tracker.keys(): self.liq_queue_tracker[i] = None def modify_cue_point(self, link): assert self.is_file(link) tnow = datetime.utcnow() link_start = link['start'] diff_td = tnow - link_start diff_sec = self.date_interval_to_seconds(diff_td) if diff_sec > 0: self.logger.debug("media item was supposed to start %s ago. Preparing to start..", diff_sec) original_cue_in_td = timedelta(seconds=float(link['cue_in'])) link['cue_in'] = self.date_interval_to_seconds(original_cue_in_td) + diff_sec def date_interval_to_seconds(self, interval): """ Convert timedelta object into int representing the number of seconds. If number of seconds is less than 0, then return 0. """ seconds = (interval.microseconds + \ (interval.seconds + interval.days * 24 * 3600) * 10 ** 6) / float(10 ** 6) if seconds < 0: seconds = 0 return seconds def clear_all_queues(self): self.telnet_liquidsoap.queue_clear_all() class UnknownMediaItemType(Exception): pass class NoQueueAvailableException(Exception): pass