Source code for coherence.backends.buzztard_control

# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# Copyright 2007, Frank Scholz <coherence@beebits.net>

from twisted.internet import protocol
from twisted.internet.task import LoopingCall
from twisted.protocols.basic import LineReceiver
from twisted.python import failure

import coherence.extern.louie as louie
from coherence import log
from coherence.extern.simple_plugin import Plugin
from coherence.upnp.core import DIDLLite
from coherence.upnp.core.DIDLLite import classChooser, Container, Resource
from coherence.upnp.core.soap_service import errorCode


[docs]class BzClient(LineReceiver, log.LogAble): logCategory = 'buzztard_client' factory = None def __init__(self, *args, **kwargs): log.LogAble.__init__(self)
[docs] def connectionMade(self): self.info("connected to Buzztard") if self.factory is None: self.error('Cannot access to BzClient.factory ' 'property...cancelled') return False self.factory.clientReady(self)
[docs] def lineReceived(self, line): self.debug("received: %s", line) if line == 'flush': louie.send('Buzztard.Response.flush', None) elif line.find('event') == 0: louie.send('Buzztard.Response.event', None, line) elif line.find('volume') == 0: louie.send('Buzztard.Response.volume', None, line) elif line.find('mute') == 0: louie.send('Buzztard.Response.mute', None, line) elif line.find('repeat') == 0: louie.send('Buzztard.Response.repeat', None, line) elif line.find('playlist') == 0: louie.send('Buzztard.Response.browse', None, line)
[docs]class BzFactory(protocol.ClientFactory, log.LogAble): logCategory = 'buzztard_factory' protocol = BzClient def __init__(self, backend): log.LogAble.__init__(self) self.backend = backend
[docs] def clientConnectionFailed(self, connector, reason): self.error('connection failed: %s', reason.getErrorMessage())
[docs] def clientConnectionLost(self, connector, reason): self.error('connection lost: %s', reason.getErrorMessage())
[docs] def startFactory(self): self.messageQueue = [] self.clientInstance = None
[docs] def clientReady(self, instance): self.info("clientReady") louie.send('Coherence.UPnP.Backend.init_completed', None, backend=self.backend) self.clientInstance = instance for msg in self.messageQueue: self.sendMessage(msg)
[docs] def sendMessage(self, msg): if self.clientInstance is not None: self.clientInstance.sendLine(msg) else: self.messageQueue.append(msg)
[docs] def rebrowse(self): self.backend.clear() self.browse()
[docs] def browse(self): self.sendMessage('browse')
[docs]class BzConnection(log.LogAble): """ a singleton class """ logCategory = 'buzztard_connection' connection = None def __init__(self, backend=None, host='localhost', port=7654): log.LogAble.__init__(self) self.debug("BzConnection __init__") def __new__(cls, *args, **kwargs): cls.debug(cls, "BzConnection __new__") obj = getattr(cls, '_instance_', None) if obj is not None: louie.send('Coherence.UPnP.Backend.init_completed', None, backend=kwargs['backend']) return obj else: obj = super(BzConnection, cls).__new__(cls, *args, **kwargs) cls._instance_ = obj obj.connection = BzFactory(kwargs['backend']) reactor.connectTCP(kwargs['host'], kwargs['port'], obj.connection) return obj
[docs]class BuzztardItem(log.LogAble): logCategory = 'buzztard_item' def __init__(self, id, name, parent, mimetype, urlbase, host, update=False): log.LogAble.__init__(self) self.id = id self.name = name self.mimetype = mimetype self.parent = parent if parent: parent.add_child(self, update=update) if parent is None: parent_id = -1 else: parent_id = parent.get_id() UPnPClass = classChooser( mimetype, sub='music') # FIXME: this is stupid self.item = UPnPClass(id, parent_id, self.name) self.child_count = 0 self.children = [] if (len(urlbase) and urlbase[-1] != '/'): urlbase += '/' # self.url = urlbase + str(self.id) self.url = self.name if self.mimetype == 'directory': self.update_id = 0 else: res = Resource(self.url, 'internal:%s:%s:*' % ( host, self.mimetype)) res.size = None self.item.res.append(res) self.item.artist = self.parent.name def __del__(self): self.debug("BuzztardItem __del__ %s %s", self.id, self.name) pass
[docs] def remove(self, store): self.debug("BuzztardItem remove %s %s %s", self.id, self.name, self.parent) while len(self.children) > 0: child = self.children.pop() self.remove_child(child) del store[int(child.id)] if self.parent: self.parent.remove_child(self) del store[int(self.id)] del self.item del self
[docs] def add_child(self, child, update=False): self.children.append(child) self.child_count += 1 if isinstance(self.item, Container): self.item.childCount += 1 if update: self.update_id += 1
[docs] def remove_child(self, child): self.debug("remove_from %d (%s) child %d (%s)", self.id, self.get_name(), child.id, child.get_name()) if child in self.children: self.child_count -= 1 if isinstance(self.item, Container): self.item.childCount -= 1 self.children.remove(child) self.update_id += 1
[docs] def get_children(self, start=0, request_count=0): if request_count == 0: return self.children[start:] else: return self.children[start:request_count]
[docs] def get_child_count(self): return self.child_count
[docs] def get_id(self): return self.id
[docs] def get_update_id(self): if hasattr(self, 'update_id'): return self.update_id else: return None
[docs] def get_path(self): return self.url
[docs] def get_name(self): return self.name
[docs] def get_parent(self): return self.parent
[docs] def get_item(self): return self.item
[docs] def get_xml(self): return self.item.toString()
def __repr__(self): if self.parent is None: parent = 'root' else: parent = str(self.parent.get_id()) return 'id: ' + str( self.id) + '/' + self.name + '/' + parent + ' ' + str( self.child_count) + ' @ ' + self.url
[docs]class BuzztardStore(log.LogAble, Plugin): logCategory = 'buzztard_store' implements = ['MediaServer'] def __init__(self, server, **kwargs): log.LogAble.__init__(self) self.next_id = 1000 self.config = kwargs self.name = kwargs.get('name', 'Buzztard') self.urlbase = kwargs.get('urlbase', '') if (len(self.urlbase) > 0 and self.urlbase[len(self.urlbase) - 1] != '/'): self.urlbase += '/' self.host = kwargs.get('host', '127.0.0.1') self.port = int(kwargs.get('port', 7654)) self.server = server self.update_id = 0 self.store = {} self.parent = None louie.connect(self.add_content, 'Buzztard.Response.browse', louie.Any) louie.connect(self.clear, 'Buzztard.Response.flush', louie.Any) self.buzztard = BzConnection(backend=self, host=self.host, port=self.port) def __repr__(self): return str(self.__class__).split('.')[-1]
[docs] def add_content(self, line): data = line.split('|')[1:] parent = self.append(data[0], 'directory', self.parent) i = 0 for label in data[1:]: self.append(':'.join((label, str(i))), 'audio/mpeg', parent) i += 1
[docs] def append(self, name, mimetype, parent): id = self.getnextID() update = False if hasattr(self, 'update_id'): update = True self.store[id] = BuzztardItem(id, name, parent, mimetype, self.urlbase, self.host, update=update) if hasattr(self, 'update_id'): self.update_id += 1 if self.server: self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) if parent: # value = '%d,%d' % (parent.get_id(),parent_get_update_id()) value = (parent.get_id(), parent.get_update_id()) if self.server: self.server.content_directory_server.set_variable( 0, 'ContainerUpdateIDs', value) if mimetype == 'directory': return self.store[id] return None
[docs] def remove(self, id): item = self.store[int(id)] parent = item.get_parent() item.remove(self.store) try: del self.store[int(id)] except (ValueError, KeyError): pass if hasattr(self, 'update_id'): self.update_id += 1 if self.server: self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) value = (parent.get_id(), parent.get_update_id()) if self.server: self.server.content_directory_server.set_variable( 0, 'ContainerUpdateIDs', value)
[docs] def clear(self): for item in self.get_by_id(1000).get_children(): self.remove(item.get_id()) self.buzztard.connection.browse()
[docs] def len(self): return len(self.store)
[docs] def get_by_id(self, id): id = int(id) if id == 0: id = 1000 try: return self.store[id] except KeyError: return None
[docs] def getnextID(self): ret = self.next_id self.next_id += 1 return ret
[docs] def upnp_init(self): self.current_connection_id = None self.parent = self.append('Buzztard', 'directory', None) source_protocols = "" if self.server: self.server.connection_manager_server.set_variable( 0, 'SourceProtocolInfo', source_protocols, default=True) self.buzztard.connection.browse()
[docs]class BuzztardPlayer(log.LogAble): logCategory = 'buzztard_player' implements = ['MediaRenderer'] vendor_value_defaults = { 'RenderingControl': {'A_ARG_TYPE_Channel': 'Master'}} vendor_range_defaults = {'RenderingControl': {'Volume': {'maximum': 100}}} def __init__(self, device, **kwargs): log.LogAble.__init__(self) self.name = kwargs.get('name', 'Buzztard MediaRenderer') self.host = kwargs.get('host', '127.0.0.1') self.port = int(kwargs.get('port', 7654)) self.player = None self.playing = False self.state = None self.duration = None self.view = [] self.tags = {} self.server = device self.poll_LC = LoopingCall(self.poll_player) louie.connect(self.event, 'Buzztard.Response.event', louie.Any) louie.connect(self.get_volume, 'Buzztard.Response.volume', louie.Any) louie.connect(self.get_mute, 'Buzztard.Response.mute', louie.Any) louie.connect(self.get_repeat, 'Buzztard.Response.repeat', louie.Any) self.buzztard = BzConnection(backend=self, host=self.host, port=self.port)
[docs] def event(self, line): infos = line.split('|')[1:] self.debug(infos) if infos[0] == 'playing': transport_state = 'PLAYING' if infos[0] == 'stopped': transport_state = 'STOPPED' if infos[0] == 'paused': transport_state = 'PAUSED_PLAYBACK' if self.server is not None: connection_id = \ self.server.connection_manager_server.lookup_avt_id( self.current_connection_id) if self.state != transport_state: self.state = transport_state if self.server is not None: self.server.av_transport_server.set_variable( connection_id, 'TransportState', transport_state) label = infos[1] position = infos[2].split('.')[0] duration = infos[3].split('.')[0] if self.server is not None: self.server.av_transport_server.set_variable( connection_id, 'CurrentTrack', 0) self.server.av_transport_server.set_variable( connection_id, 'CurrentTrackDuration', duration) self.server.av_transport_server.set_variable( connection_id, 'CurrentMediaDuration', duration) self.server.av_transport_server.set_variable( connection_id, 'RelativeTimePosition', position) self.server.av_transport_server.set_variable( connection_id, 'AbsoluteTimePosition', position) try: self.server.rendering_control_server.set_variable( connection_id, 'Volume', int(infos[4])) except Exception: pass try: if infos[5] in ['on', '1', 'true', 'True', 'yes', 'Yes']: mute = True else: mute = False self.server.rendering_control_server.set_variable( connection_id, 'Mute', mute) except Exception: pass try: if infos[6] in ['on', '1', 'true', 'True', 'yes', 'Yes']: self.server.av_transport_server.set_variable( connection_id, 'CurrentPlayMode', 'REPEAT_ALL') else: self.server.av_transport_server.set_variable( connection_id, 'CurrentPlayMode', 'NORMAL') except Exception: pass
def __repr__(self): return str(self.__class__).split('.')[-1]
[docs] def poll_player(self): self.buzztard.connection.sendMessage('status')
[docs] def load(self, uri, metadata=None): self.debug("load %s %s", uri, metadata) self.duration = None self.metadata = metadata connection_id = self.server.connection_manager_server.lookup_avt_id( self.current_connection_id) self.server.av_transport_server.set_variable( connection_id, 'CurrentTransportActions', 'Play,Stop') self.server.av_transport_server.set_variable( connection_id, 'NumberOfTracks', 1) self.server.av_transport_server.set_variable( connection_id, 'CurrentTrackURI', uri) self.server.av_transport_server.set_variable( connection_id, 'AVTransportURI', uri) self.server.av_transport_server.set_variable( connection_id, 'AVTransportURIMetaData', metadata) self.server.av_transport_server.set_variable( connection_id, 'CurrentTrackURI', uri) self.server.av_transport_server.set_variable( connection_id, 'CurrentTrackMetaData', metadata)
[docs] def start(self, uri): self.load(uri) self.play()
[docs] def stop(self): self.buzztard.connection.sendMessage('stop')
[docs] def play(self): connection_id = self.server.connection_manager_server.lookup_avt_id( self.current_connection_id) label_id = self.server.av_transport_server.get_variable( 'CurrentTrackURI', connection_id).value id = '0' if ':' in label_id: label, id = label_id.split(':') self.buzztard.connection.sendMessage('play|%s' % id)
[docs] def pause(self): self.buzztard.connection.sendMessage('pause')
[docs] def seek(self, location): """ @param location: simple number = time to seek to, in seconds +nL = relative seek forward n seconds -nL = relative seek backwards n seconds """
[docs] def mute(self): self.buzztard.connection.sendMessage('set|mute|on')
[docs] def unmute(self): self.buzztard.connection.sendMessage('set|mute|off')
[docs] def get_mute(self, line): infos = line.split('|')[1:] if infos[0] in ['on', '1', 'true', 'True', 'yes', 'Yes']: mute = True else: mute = False self.server.rendering_control_server.set_variable( 0, 'Mute', mute)
[docs] def get_repeat(self, line): infos = line.split('|')[1:] if infos[0] in ['on', '1', 'true', 'True', 'yes', 'Yes']: self.server.av_transport_server.set_variable( 0, 'CurrentPlayMode', 'REPEAT_ALL') else: self.server.av_transport_server.set_variable( 0, 'CurrentPlayMode', 'NORMAL')
[docs] def set_repeat(self, playmode): if playmode in ['REPEAT_ONE', 'REPEAT_ALL']: self.buzztard.connection.sendMessage('set|repeat|on') else: self.buzztard.connection.sendMessage('set|repeat|off')
[docs] def get_volume(self, line): infos = line.split('|')[1:] self.server.rendering_control_server.set_variable( 0, 'Volume', int(infos[0]))
[docs] def set_volume(self, volume): volume = int(volume) if volume < 0: volume = 0 if volume > 100: volume = 100 self.buzztard.connection.sendMessage('set|volume|%d' % volume)
[docs] def upnp_init(self): self.current_connection_id = None self.server.connection_manager_server.set_variable( 0, 'SinkProtocolInfo', ['internal:%s:audio/mpeg:*' % self.host], default=True) self.server.av_transport_server.set_variable( 0, 'TransportState', 'NO_MEDIA_PRESENT', default=True) self.server.av_transport_server.set_variable( 0, 'TransportStatus', 'OK', default=True) self.server.av_transport_server.set_variable( 0, 'CurrentPlayMode', 'NORMAL', default=True) self.server.av_transport_server.set_variable( 0, 'CurrentTransportActions', '', default=True) self.buzztard.connection.sendMessage('get|volume') self.buzztard.connection.sendMessage('get|mute') self.buzztard.connection.sendMessage('get|repeat') self.poll_LC.start(1.0, True)
[docs] def upnp_Play(self, *args, **kwargs): InstanceID = int(kwargs['InstanceID']) Speed = int(kwargs['Speed']) self.play() return {}
[docs] def upnp_Pause(self, *args, **kwargs): InstanceID = int(kwargs['InstanceID']) self.pause() return {}
[docs] def upnp_Stop(self, *args, **kwargs): InstanceID = int(kwargs['InstanceID']) self.stop() return {}
[docs] def upnp_SetAVTransportURI(self, *args, **kwargs): InstanceID = int(kwargs['InstanceID']) CurrentURI = kwargs['CurrentURI'] CurrentURIMetaData = kwargs['CurrentURIMetaData'] local_protocol_info = \ self.server.connection_manager_server.get_variable( 'SinkProtocolInfo').value.split(',') if len(CurrentURIMetaData) == 0: self.load(CurrentURI, CurrentURIMetaData) return {} else: elt = DIDLLite.DIDLElement.fromString(CurrentURIMetaData) print(elt.numItems()) if elt.numItems() == 1: item = elt.getItems()[0] for res in item.res: print(res.protocolInfo, local_protocol_info) if res.protocolInfo in local_protocol_info: self.load(CurrentURI, CurrentURIMetaData) return {} return failure.Failure(errorCode(714))
[docs] def upnp_SetMute(self, *args, **kwargs): InstanceID = int(kwargs['InstanceID']) Channel = kwargs['Channel'] DesiredMute = kwargs['DesiredMute'] if DesiredMute in ['TRUE', 'True', 'true', '1', 'Yes', 'yes']: self.mute() else: self.unmute() return {}
[docs] def upnp_SetVolume(self, *args, **kwargs): InstanceID = int(kwargs['InstanceID']) Channel = kwargs['Channel'] DesiredVolume = int(kwargs['DesiredVolume']) self.set_volume(DesiredVolume) return {}
[docs]def test_init_complete(backend): print("Houston, we have a touchdown!") backend.buzztard.sendMessage('browse')
[docs]def main(): louie.connect(test_init_complete, 'Coherence.UPnP.Backend.init_completed', louie.Any) f = BuzztardStore(None) f.parent = f.append('Buzztard', 'directory', None) print(f.parent) print(f.store) f.add_content('playlist|test label|start|stop') print(f.store) f.clear() print(f.store) f.add_content('playlist|after flush label|flush-start|flush-stop') print(f.store)
# def got_upnp_result(result): # print "upnp", result # f.upnp_init() # print f.store # r = f.upnp_Browse(BrowseFlag='BrowseDirectChildren', # RequestedCount=0, # StartingIndex=0, # ObjectID=0, # SortCriteria='*', # Filter='') # got_upnp_result(r) if __name__ == '__main__': from twisted.internet import reactor reactor.callWhenRunning(main) reactor.run()