"""App config for organ """ from __future__ import with_statement import time from Queue import Queue from threading import Thread, Lock import urlparse import httplib import logging import sqlobject try: import json except ImportError: import simplejson as json import calliope.config as config from calliope.recording import Recording from calliope.tuner import Tuner from calliope.channeltuner import ChannelTuner from calliope.channel import Channel from calliope.scheduler import Scheduler import calliope.recorder as channels from calliope.ZeroconfService import ZeroconfBrowser import calliope.muddleware as muddleware alert = None log = logging.getLogger("calliope.organ") def setup(mapper): opfx = mapper.prefix # Recordings mapper.prefix = "/api/recordings" mapper.add("[/]", GET=get_all_ids, POST=new_item, OPTIONS=get_options) mapper.add("/all", GET=get_all) mapper.add("/{recid:digits}[/]", GET=get_item, PUT=update_item, DELETE=del_item, OPTIONS=get_options) # Cache of remote tuner capabilities # Do we actually need this, except for debugging purposes? #mapper.add("/tuners/{tunerhost}/{tunerid:digits}/channels[/]", # GET=remote_tuners.get_all) # Cache of known channels mapper.prefix = "/api/channels" mapper.add("/all[/]", GET=get_channels, OPTIONS=get_options) mapper.add("/by-id/{chanid:digits}[/]", GET=get_channel_by_id) mapper.add("/by-name/{chname}[/]", GET=get_channel_by_name) mapper.prefix = opfx # This lock protects the central tuners list, as updating it is # quite a long process, and would seriously bugger up the tuner # allocator if the two happened together. We don't care quite so # much about ordinary data requests. global tuners_lock tuners_lock = Lock() # Kick off a scheduler thread global alert alert = Queue(0) # Infinite size sch = Scheduler(alert, tuners_lock) sch.start() # Kick off a zeroconf listener thread zc_listener = ZCListener(tuners_lock) zc_listener.start() def get_all_ids(req, res): res.data = [rec.id for rec in Recording.select()] def get_all(req, res): res.data = [rec.get_data() for rec in Recording.select()] def get_item(req, res): recid = int(req['wsgiorg.routing_args'][1]['recid']) # [1] is named_args try: rec = Recording.get(recid) res.data = rec.get_data() except sqlobject.SQLObjectNotFound: res.result = "404 Not found" res.data = "Not found" def get_options(req, res): log.info("OPTIONS requested") res.headers['Access-Control-Allow-Origin'] = "*" res.headers['Access-Control-Allow-Methods'] = "OPTIONS, POST, GET" res.headers['Access-Control-Allow-Headers'] = "content-type" def update_item(req, res): recid = int(req['wsgiorg.routing_args'][1]['recid']) # [1] is named_args rec = Recording.get(recid) if rec is None: res.result = "404 Not found" res.data = "Not found" else: content_len = int(req['CONTENT_LENGTH']) text = req['wsgi.input'].read(content_len) struct = json.loads(text) for elt in ('title', 'subtitle', 'series', 'episode', 'start', 'end'): if elt in struct: setattr(rec, elt, struct[elt]) if 'chanid' in struct: rec.channel = Channel.get(struct['chanid']) alert.put(("Modified", rec)) res.data = rec.get_data() def del_item(req, res): recid = int(req['wsgiorg.routing_args'][1]['recid']) # [1] is named_args rec = Recording.get(recid) if rec is None: res.result = "404 Not found" res.data = "Not found" else: alert.put(("Deleted", rec.start)) rec.destroySelf() def new_item(req, res): content_len = int(req['CONTENT_LENGTH']) text = req['wsgi.input'].read(content_len) struct = json.loads(text) chan = Channel.get(struct['chanid']) rec = Recording(start=struct['start'], end=struct['end'], title=struct['title'], channel=chan) if 'subtitle' in struct: rec.subtitle = struct['subtitle'] if 'series' in struct: rec.series = struct['series'] if 'episode' in struct: rec.episode = struct['episode'] res.data = rec.get_data() res.headers['Access-Control-Allow-Origin'] = "*" res.headers['Access-Control-Allow-Methods'] = "OPTIONS, POST, GET" alert.put(("Added", rec)) ####################################### def get_channels(req, res): """Get the full list of channels we know about """ res.data = [ { "id": ch.id, "label": ch.label } for ch in Channel.select() ] res.headers['Access-Control-Allow-Origin'] = "*" res.headers['Access-Control-Allow-Methods'] = "OPTIONS, GET" def get_channel_by_id(req, res): """Get data on a single channel """ chid = int(req['wsgiorg.routing_args'][1]['chanid']) try: chan = Channel.get(chid) res.data = chan.get_data() except sqlobject.SQLObjectNotFound: res.result = "404 Not found" res.data = "Not found" def get_channel_by_name(req, res): """Get data on a single channel """ chid = req['wsgiorg.routing_args'][1]['chname'] chan = Channel.select(Channel.q.label == chid).getOne(None) if chan is not None: res.data = chan.get_data() else: res.result = "404 Not found" res.data = "Not found" class ZCListener(ZeroconfBrowser, Thread): def __init__(self, tuner_lock): ZeroconfBrowser.__init__(self) Thread.__init__(self, name="Zeroconf listener", ) self.setDaemon(True) self.tuner_lock = tuner_lock def run(self): self.browse("_calliope._tcp") self() def removal(self, interface, protocol, name, type, domain, flags, handler): """A service has vanished. """ log.info("Lost a calliope recorder service") with self.tuner_lock: # FIXME: Mark removed tuners as unavailable pass def resolved(self, interface, protocol, name, type, domain, host, aprotocol, address, port, txt, flags, handler): """We have found a service. Go forth and load all the tuner's data from it, discarding previous frequency data. """ log.info("Found a Calliope recorder service") # FIXME: Ignore anything running in this service, as we # already have the data for that in the DB # Extract the text fields from the ZC records zcdata = {} for t in txt: x = str("".join((chr(c) for c in t))) key, value = x.split("=", 1) zcdata[key] = value with self.tuner_lock: # First, get hold of the list of tuners at this new service urlparts = [ zcdata["protocol"], host + ":" + str(port), zcdata["path"], None, None ] base_url = urlparse.urlunsplit(urlparts) conn = httplib.HTTPConnection(host, port) log.debug("Requesting path at %s", zcdata["path"]) conn.request("GET", zcdata["path"]) resp = conn.getresponse() tuner_ids = json.loads(resp.read()) log.debug("Got tuners: " + str(tuner_ids)) for tid in tuner_ids: # Try to find an existing tuner in our DB with this # URL url = base_url + "/%d" % tid path = zcdata["path"] + "/%d" % tid try: t = Tuner.select(Tuner.q.url == url).getOne() log.debug("Existing tuner found for this URL: updating") except sqlobject.SQLObjectNotFound: # We end up here if there was no tuner with that # URL, so we ignore the exception and create a new # one. log.debug("No tuner found for url %s, creating new one", url) t = Tuner(local=False) log.debug("Loading tuner %d" % tid) # Read the tuner's base information headers = { "If-Modified-Since": time.strftime(muddleware.RFC_2822_DATE, time.gmtime(t.last_update)) } log.debug("Requesting path at %s", path) conn.request("GET", path, "", headers) resp = conn.getresponse() if resp.status == 200: # We have new data, so set the tuner's new details tuner_data = json.loads(resp.read()) #t.active = True for key in ("adapter", "manufacturer", "product", "url", "tid", "uuid"): setattr(t, key, tuner_data[key]) # Get the remote's channels list, for cross-referencing path += "/channels/all" log.debug("Requesting path at %s", path) conn.request("GET", path) resp = conn.getresponse() channel_list = json.loads(resp.read()) channels = {} for c in channel_list: channels[c["id"]] = c["label"] # Read the tuner's channel/tuner data path += "_tuning" log.debug("Requesting path at %s", path) conn.request("GET", path) resp = conn.getresponse() ct_list = json.loads(resp.read()) for ct_data in ct_list: log.debug("Adding C/T for %s" % channels[ct_data["channelID"]]) ct = ChannelTuner(tuner=t) ct.setup(channels[ct_data["channelID"]], ct_data["freq"], None, None, None, None, None, None, None, None, ct_data["vpid"], ct_data["apid"], ct_data["tpid"] ) t.last_update = int(time.time()) elif resp.status == 304: # This is good, we don't need any further updates log.info("No updates required for %s", tid)