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>
# Copyright 2018, Pol Canelles <canellestudi@gmail.com>

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

from eventdispatcher import EventDispatcher

from coherence import log
from coherence.backend import Backend
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, EventDispatcher, log.LogAble): ''' .. versionchanged:: 0.9.0 * Migrated from louie/dispatcher to EventDispatcher * The emitted events changed: - Buzztard.Response.flush => response_flush - Buzztard.Response.event => response_event - Buzztard.Response.volume => response_volume - Buzztard.Response.mute=> response_mute - Buzztard.Response.repeat => response_repeat - Buzztard.Response.browse => response_browse ''' logCategory = 'buzztard_client' factory = None def __init__(self, *args, **kwargs): EventDispatcher.__init__(self) log.LogAble.__init__(self) self.register_event( 'response_flush', 'response_event', 'response_volume', 'response_mute', 'response_repeat', 'response_browse', )
[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(f'received: {line}') if line == 'flush': self.dispatch_event('response_flush') elif line.find('event') == 0: self.dispatch_event('response_event', line) elif line.find('volume') == 0: self.dispatch_event('response_volume', line) elif line.find('mute') == 0: self.dispatch_event('response_mute', line) elif line.find('repeat') == 0: self.dispatch_event('response_repeat', line) elif line.find('playlist') == 0: self.dispatch_event('response_browse', line)
[docs]class BzFactory(protocol.ClientFactory, log.LogAble): ''' .. versionchanged:: 0.9.0 Migrated from louie/dispatcher to EventDispatcher ''' logCategory = 'buzztard_factory' protocol = BzClient def __init__(self, backend): log.LogAble.__init__(self) self.backend = backend
[docs] def clientConnectionFailed(self, connector, reason): self.error(f'connection failed: {reason.getErrorMessage()}')
[docs] def clientConnectionLost(self, connector, reason): self.error(f'connection lost: {reason.getErrorMessage()}')
[docs] def startFactory(self): self.messageQueue = [] self.clientInstance = None
[docs] def clientReady(self, instance): self.info('clientReady') self.backend.init_completed = True 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 .. versionchanged:: 0.9.0 Migrated from louie/dispatcher to EventDispatcher ''' 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): print(cls, 'BzConnection __new__') obj = getattr(cls, '_instance_', None) if obj is not None: if kwargs['backend']: kwargs['backend'].init_completed = True return obj else: obj = super(BzConnection, cls).__new__(cls) 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, f'internal:{host}:{self.mimetype}:*') res.size = None self.item.res.append(res) self.item.artist = self.parent.name def __del__(self): self.debug(f'BuzztardItem __del__ {self.id} {self.name}') pass
[docs] def remove(self, store): self.debug(f'BuzztardItem remove {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): if self.item.childCount is None: self.item.childCount = 0 self.item.childCount += 1 if update: self.update_id += 1
[docs] def remove_child(self, child): self.debug( f'remove_from {self.id:d} ({self.get_name()}) ' f'child {child.id:d} ({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(Backend): ''' .. versionchanged:: 0.9.0 * Migrated from louie/dispatcher to EventDispatcher * Introduced :class:`~coherence.backend.Backend`'s inheritance ''' logCategory = 'buzztard_store' implements = ['MediaServer'] def __init__(self, server, **kwargs): Backend.__init__(self, server, **kwargs) 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 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) self.buzztard.connection.protocol.bind( response_browse=self.add_content) self.buzztard.connection.protocol.bind( response_browse=self.clear) 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): ''' .. versionchanged:: 0.9.0 Migrated from louie/dispatcher to EventDispatcher ''' 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) self.buzztard = BzConnection( backend=self, host=self.host, port=self.port) self.buzztard.connection.protocol.bind( response_event=self.event) self.buzztard.connection.protocol.bind( response_volume=self.get_volume) self.buzztard.connection.protocol.bind( response_mute=self.get_mute) self.buzztard.connection.protocol.bind( response_repeat=self.get_repeat)
[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(f'load {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(f'play|{id}')
[docs] def pause(self): self.buzztard.connection.sendMessage('pause')
[docs] def seek(self, location): ''' Args: location (int): 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(f'set|volume|{volume:d}')
[docs] def upnp_init(self): self.current_connection_id = None self.server.connection_manager_server.set_variable( 0, 'SinkProtocolInfo', [f'internal:{self.host}:audio/mpeg:*'], 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 on_init_complete(backend): print('Houston, we have a touchdown!') backend.buzztard.sendMessage('browse')
[docs]def main(): ''' .. versionchanged:: 0.9.0 Migrated from louie/dispatcher to EventDispatcher ''' f = BuzztardStore(None) f.bind(init_completed=on_init_complete) f.parent = f.append('Buzztard', 'directory', None) print(f.parent) print(f'f.store is: {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()