import urllib import logging import logging.config import json import time import datetime import os import sys import shutil import socket import pytz import signal from configobj import ConfigObj from poster.encode import multipart_encode from poster.streaminghttp import register_openers import urllib2 from subprocess import Popen from threading import Thread import mutagen from api_clients import api_client # For RabbitMQ from kombu.connection import BrokerConnection from kombu.messaging import Exchange, Queue, Consumer, Producer # configure logging try: logging.config.fileConfig("logging.cfg") except Exception, e: print 'Error configuring logging: ', e sys.exit() # loading config file try: config = ConfigObj('/etc/airtime/recorder.cfg') except Exception, e: logger = logging.getLogger() logger.error('Error loading config file: %s', e) sys.exit() def getDateTimeObj(time): timeinfo = time.split(" ") date = timeinfo[0].split("-") time = timeinfo[1].split(":") date = map(int, date) time = map(int, time) return datetime.datetime(date[0], date[1], date[2], time[0], time[1], time[2], 0, None) class ShowRecorder(Thread): def __init__ (self, show_instance, show_name, filelength, start_time): Thread.__init__(self) self.api_client = api_client.api_client_factory(config) self.filelength = filelength self.start_time = start_time self.show_instance = show_instance self.show_name = show_name self.logger = logging.getLogger('root') self.p = None def record_show(self): length = str(self.filelength)+".0" filename = self.start_time filename = filename.replace(" ", "-") if config["record_file_type"] in ["mp3", "ogg"]: filetype = config["record_file_type"] else: filetype = "ogg"; filepath = "%s%s.%s" % (config["base_recorded_files"], filename, filetype) br = config["record_bitrate"] sr = config["record_samplerate"] c = config["record_channels"] ss = config["record_sample_size"] #-f:16,2,44100 #-b:256 command = "ecasound -f:%s,%s,%s -i alsa -o %s,%s000 -t:%s" % (ss, c, sr, filepath, br, length) args = command.split(" ") self.logger.info("starting record") self.logger.info("command " + command) self.p = Popen(args) #blocks at the following line until the child process #quits code = self.p.wait() self.logger.info("finishing record, return code %s", self.p.returncode) code = self.p.returncode self.p = None return code, filepath def cancel_recording(self): #add 3 second delay before actually cancelling the show. The reason #for this is because it appears that ecasound starts 1 second later than #it should, and therefore this method is sometimes incorrectly called 1 #second before the show ends. #time.sleep(3) #send signal interrupt (2) self.logger.info("Show manually cancelled!") if (self.p is not None): self.p.send_signal(signal.SIGINT) #if self.p is defined, then the child process ecasound is recording def is_recording(self): return (self.p is not None) def upload_file(self, filepath): filename = os.path.split(filepath)[1] # Register the streaming http handlers with urllib2 register_openers() # headers contains the necessary Content-Type and Content-Length # datagen is a generator object that yields the encoded parameters datagen, headers = multipart_encode({"file": open(filepath, "rb"), 'name': filename, 'show_instance': self.show_instance}) self.api_client.upload_recorded_show(datagen, headers) def set_metadata_and_save(self, filepath): try: date = self.start_time md = date.split(" ") time = md[1].replace(":", "-") self.logger.info("time: %s" % time) name = time+"-"+self.show_name artist = api_client.encode_to("Airtime Show Recorder",'utf-8') #set some metadata for our file daemon recorded_file = mutagen.File(filepath, easy=True) recorded_file['title'] = name recorded_file['artist'] = artist recorded_file['date'] = md[0] recorded_file['tracknumber'] = self.show_instance recorded_file.save() except Exception, e: self.logger.error("Exception: %s", e) def run(self): code, filepath = self.record_show() if code == 0: try: self.logger.info("Preparing to upload %s" % filepath) self.set_metadata_and_save(filepath) self.upload_file(filepath) os.remove(filepath) except Exception, e: self.logger.error(e) else: self.logger.info("problem recording show") os.remove(filepath) class CommandListener(): def __init__(self): #Thread.__init__(self) self.api_client = api_client.api_client_factory(config) self.api_client.register_component("show-recorder") self.logger = logging.getLogger('root') self.sr = None self.current_schedule = {} self.shows_to_record = {} self.time_till_next_show = 3600 self.logger.info("RecorderFetch: init complete") self.server_timezone = ''; def init_rabbit_mq(self): self.logger.info("Initializing RabbitMQ stuff") try: schedule_exchange = Exchange("airtime-show-recorder", "direct", durable=True, auto_delete=True) schedule_queue = Queue("recorder-fetch", exchange=schedule_exchange, key="foo") self.connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], config["rabbitmq_vhost"]) channel = self.connection.channel() consumer = Consumer(channel, schedule_queue) consumer.register_callback(self.handle_message) consumer.consume() except Exception, e: self.logger.error(e) return False return True def handle_message(self, body, message): # ACK the message to take it off the queue message.ack() self.logger.info("Received command from RabbitMQ: " + message.body) m = json.loads(message.body) command = m['event_type'] self.logger.info("Handling command: " + command) if(command == 'update_schedule'): temp = m['shows'] if temp is not None: self.parse_shows(temp) self.server_timezone = m['server_timezone'] elif(command == 'cancel_recording'): if self.sr.is_recording(): self.sr.cancel_recording() def parse_shows(self, shows): self.logger.info("Parsing show schedules...") self.shows_to_record = {} for show in shows: show_starts = getDateTimeObj(show[u'starts']) show_end = getDateTimeObj(show[u'ends']) time_delta = show_end - show_starts self.shows_to_record[show[u'starts']] = [time_delta, show[u'instance_id'], show[u'name']] delta = self.get_time_till_next_show() # awake at least 5 seconds prior to the show start self.time_till_next_show = delta - 5 self.logger.info(self.shows_to_record) def get_time_till_next_show(self): if len(self.shows_to_record) != 0: tnow = datetime.datetime.utcnow() sorted_show_keys = sorted(self.shows_to_record.keys()) start_time = sorted_show_keys[0] next_show = getDateTimeObj(start_time) delta = next_show - tnow out = delta.seconds self.logger.debug("Next show %s", next_show) self.logger.debug("Now %s", tnow) else: out = 3600 return out def start_record(self): if len(self.shows_to_record) != 0: try: delta = self.get_time_till_next_show() self.logger.debug("sleeping %s seconds until show", delta) time.sleep(delta) sorted_show_keys = sorted(self.shows_to_record.keys()) start_time = sorted_show_keys[0] show_length = self.shows_to_record[start_time][0] show_instance = self.shows_to_record[start_time][1] show_name = self.shows_to_record[start_time][2] T = pytz.timezone(self.server_timezone) start_time_on_UTC = getDateTimeObj(start_time) start_time_on_server = start_time_on_UTC.replace(tzinfo=pytz.utc).astimezone(T) start_time_formatted = '%(year)d-%(month)02d-%(day)02d %(hour)02d:%(min)02d:%(sec)02d' % \ {'year': start_time_on_server.year, 'month': start_time_on_server.month, 'day': start_time_on_server.day,\ 'hour': start_time_on_server.hour, 'min': start_time_on_server.minute, 'sec': start_time_on_server.second} self.sr = ShowRecorder(show_instance, show_name, show_length.seconds, start_time_formatted) self.sr.start() #remove show from shows to record. del self.shows_to_record[start_time] time_till_next_show = self.get_time_till_next_show() self.time_till_next_show = time_till_next_show except Exception,e : self.logger.error(e) else: self.logger.debug("No recording scheduled...") """ Main loop of the thread: Wait for schedule updates from RabbitMQ, but in case there arent any, poll the server to get the upcoming schedule. """ def run(self): self.logger.info("Started...") while not self.init_rabbit_mq(): self.logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds") time.sleep(5) # Bootstrap: since we are just starting up, we need to grab the # most recent schedule. After that we can just wait for updates. try: temp = self.api_client.get_shows_to_record() if temp is not None: shows = temp['shows'] self.parse_shows(shows) self.logger.info("Bootstrap complete: got initial copy of the schedule") except Exception, e: self.logger.error(e) loops = 1 while True: self.logger.info("Loop #%s", loops) try: # block until 5 seconds before the next show start self.connection.drain_events(timeout=self.time_till_next_show) except socket.timeout, s: self.logger.info(s) # start recording self.start_record() except Exception, e: self.logger.info(e) time.sleep(3) loops += 1 if __name__ == '__main__': cl = CommandListener() cl.run()